@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.
- package/LICENSE +30 -0
- package/README.md +49 -25
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1165 -1329
- package/lib/fs.js +307 -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 +18 -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 +36 -11
- 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 -99
- 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 +137 -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 -232
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
|
@@ -80,6 +80,16 @@ export interface ClusterResult {
|
|
|
80
80
|
stderr?: string;
|
|
81
81
|
durationMs?: number;
|
|
82
82
|
error?: string;
|
|
83
|
+
/** Stable machine code for a per-host failure (B-14) — lets callers retry
|
|
84
|
+
* on an auth/TOFU gate instead of parsing the human message. */
|
|
85
|
+
code?: 'NEEDS_PASSWORD' | 'HOST_KEY_UNKNOWN' | 'HOST_KEY_MISMATCH' | 'ALIAS_NOT_FOUND' | 'ABORTED' | 'TIMEOUT' | 'ERROR';
|
|
86
|
+
/** Which secret the host needs when `code` is NEEDS_PASSWORD. */
|
|
87
|
+
secret?: 'password' | 'passphrase';
|
|
88
|
+
/** Server-key fingerprint for HOST_KEY_UNKNOWN / HOST_KEY_MISMATCH. */
|
|
89
|
+
hostKeyFingerprint?: string;
|
|
90
|
+
/** Expected/actual fingerprints for HOST_KEY_MISMATCH. */
|
|
91
|
+
expected?: string;
|
|
92
|
+
actual?: string;
|
|
83
93
|
}
|
|
84
94
|
/** SFTP transfer progress frame (upload stream). */
|
|
85
95
|
export interface TransferProgress {
|
|
@@ -227,10 +237,18 @@ export type KnownHostAction = 'trust' | 'forget';
|
|
|
227
237
|
export type TransferStreamLine = {
|
|
228
238
|
type: 'progress';
|
|
229
239
|
progress: TransferProgress;
|
|
240
|
+
}
|
|
241
|
+
/** The final remote rename is about to start; loss after this point means
|
|
242
|
+
* the target's result is unknown until inspected. */
|
|
243
|
+
| {
|
|
244
|
+
type: 'commit';
|
|
230
245
|
} | {
|
|
231
246
|
type: 'result';
|
|
232
247
|
ok: boolean;
|
|
233
248
|
transferredBytes?: number;
|
|
234
249
|
error?: string;
|
|
250
|
+
code?: string;
|
|
251
|
+
secret?: 'password' | 'passphrase';
|
|
252
|
+
hostKeyFingerprint?: string;
|
|
235
253
|
};
|
|
236
254
|
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -21,8 +21,8 @@ export interface SshRoutesDeps {
|
|
|
21
21
|
knownHosts?: KnownHostsStore;
|
|
22
22
|
/** Credential vault (optional; enables /api/dsh-ssh/vault endpoints). */
|
|
23
23
|
vault?: import('./vault.ts').Vault;
|
|
24
|
-
/** SSH-bound workspace
|
|
25
|
-
ledger?: import('../
|
|
24
|
+
/** SSH-bound workspace record source (host-delete reference check). */
|
|
25
|
+
ledger?: import('../backend.ts').WorkspaceStoreView;
|
|
26
26
|
/** Temp dir for upload/download staging (tests inject a sandbox). */
|
|
27
27
|
stagingDir?: string;
|
|
28
28
|
/** Hard cap on ACTUAL upload bytes accepted (chunked requests included).
|
|
@@ -32,13 +32,21 @@ export interface SshRoutesDeps {
|
|
|
32
32
|
* remote environment cache) can drop that alias. */
|
|
33
33
|
onHostInvalidated?: (alias: string) => void;
|
|
34
34
|
}
|
|
35
|
+
/** Every /api/dsh-ssh route plus the terminal upgrade, owned by one call. */
|
|
36
|
+
export interface SshRoutes {
|
|
37
|
+
routes: WebRoute[];
|
|
38
|
+
upgrade: WebUpgradeRoute;
|
|
39
|
+
/**
|
|
40
|
+
* Release this instance's terminal resources: refuse new upgrades, close
|
|
41
|
+
* every live terminal session (and its SSH shell), close the WebSocketServer.
|
|
42
|
+
* Idempotent — a second call resolves without touching anything.
|
|
43
|
+
*/
|
|
44
|
+
dispose: () => Promise<void>;
|
|
45
|
+
}
|
|
35
46
|
/**
|
|
36
47
|
* Build every /api/dsh-ssh route (exact paths) plus the terminal upgrade.
|
|
37
48
|
* @param deps - store, engine, staging dir.
|
|
38
|
-
* @returns routes
|
|
49
|
+
* @returns routes, the upgrade route, and this instance's disposer.
|
|
39
50
|
*/
|
|
40
|
-
export declare function makeRoutes(deps: SshRoutesDeps):
|
|
41
|
-
routes: WebRoute[];
|
|
42
|
-
upgrade: WebUpgradeRoute;
|
|
43
|
-
};
|
|
51
|
+
export declare function makeRoutes(deps: SshRoutesDeps): SshRoutes;
|
|
44
52
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import type { ClientLease } from '../connection/lease.ts';
|
|
3
|
+
import type { TransferProgress } from '../protocol.ts';
|
|
4
|
+
/** Lets an SFTP operation declare the point after which replay is unsafe. */
|
|
5
|
+
export interface SftpOperationControl {
|
|
6
|
+
markCommitted(): void;
|
|
7
|
+
}
|
|
8
|
+
/** Options for one access.withClient() call. */
|
|
9
|
+
export interface SftpClientOptions {
|
|
10
|
+
/** Total acquire+operation attempt budget (default 3). */
|
|
11
|
+
attempts?: number;
|
|
12
|
+
retryPolicy?: 'never' | 'connect-only' | 'idempotent';
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Narrow connection dependency of the SFTP component: the pooled lease
|
|
17
|
+
* acquisition plus the engine's retry/replay policy. The component owns the
|
|
18
|
+
* lease for the whole operation (including the lifetime of a returned read
|
|
19
|
+
* stream) and never reaches into the pool's bookkeeping.
|
|
20
|
+
*/
|
|
21
|
+
export interface SftpClientAccess {
|
|
22
|
+
withClient<T>(alias: string, fn: (client: Client, control: SftpOperationControl) => Promise<T>, options?: SftpClientOptions): Promise<T>;
|
|
23
|
+
acquire(alias: string, options: {
|
|
24
|
+
kind: 'operation' | 'stream';
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<ClientLease>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The engine knobs the SFTP component reads. Keyed exactly like the engine's
|
|
30
|
+
* resolved options, so every SFTP deadline/concurrency keeps one source of
|
|
31
|
+
* truth.
|
|
32
|
+
*/
|
|
33
|
+
export interface SftpOptions {
|
|
34
|
+
sftpConcurrency: number;
|
|
35
|
+
sftpOpenTimeoutMs: number;
|
|
36
|
+
sftpOperationTimeoutMs: number;
|
|
37
|
+
sftpReadTimeoutMs: number;
|
|
38
|
+
maxReadFileBytes: number;
|
|
39
|
+
sftpTransferIdleTimeoutMs: number;
|
|
40
|
+
sftpRecursiveRmTimeoutMs: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Owns every SFTP operation for the engine: one cached subsystem channel per
|
|
44
|
+
* live pooled client (plus its cancellation), the per-request deadlines, and
|
|
45
|
+
* the upload/download/ls/stat/read/write/mkdir/rm/rename surface.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SftpService {
|
|
48
|
+
private readonly access;
|
|
49
|
+
private readonly sftpOpts;
|
|
50
|
+
/**
|
|
51
|
+
* One cached SFTP subsystem channel per live client. `Client.sftp()` opens a
|
|
52
|
+
* NEW subsystem channel on every call and OpenSSH caps open sessions per
|
|
53
|
+
* connection (MaxSessions, default 10) —reopening SFTP per operation lets
|
|
54
|
+
* channels pile up on the pooled long-lived connection until listing/reading
|
|
55
|
+
* fails intermittently. Caching one channel per client fixes that; the pool
|
|
56
|
+
* drops the cache via onClientDisposed when a connection is torn down.
|
|
57
|
+
*/
|
|
58
|
+
private readonly cache;
|
|
59
|
+
constructor(access: SftpClientAccess, sftpOpts: SftpOptions);
|
|
60
|
+
/** Upload one local file (or directory tree) to a remote path. */
|
|
61
|
+
upload(alias: string, localPath: string, remotePath: string, recursive: boolean, onProgress?: (progress: TransferProgress) => void, signal?: AbortSignal): Promise<{
|
|
62
|
+
bytes: number;
|
|
63
|
+
files: number;
|
|
64
|
+
}>;
|
|
65
|
+
/** Download one remote file to a local path. */
|
|
66
|
+
download(alias: string, remotePath: string, localPath: string, onProgress?: (progress: TransferProgress) => void, signal?: AbortSignal): Promise<{
|
|
67
|
+
bytes: number;
|
|
68
|
+
}>;
|
|
69
|
+
/** List a remote directory (file browser). Bounded by a timeout so a
|
|
70
|
+
* stalled SFTP request fails instead of leaving the file tree spinning. */
|
|
71
|
+
ls(alias: string, path: string, signal?: AbortSignal): Promise<import('../protocol.ts').RemoteDirEntry[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve many remote paths to their canonical form in one SFTP pass
|
|
74
|
+
* (P1-26): one lease + one batch of `sftp.realpath` calls instead of N
|
|
75
|
+
* `realpath` execs. A path that cannot resolve (dangling symlink, vanished
|
|
76
|
+
* entry) fails the whole batch — callers treat an unresolvable listing as
|
|
77
|
+
* an error rather than silently using an uncanonical path.
|
|
78
|
+
*/
|
|
79
|
+
realpaths(alias: string, remotePaths: readonly string[], signal?: AbortSignal): Promise<string[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Classify readdir entries, following symlinks so a link to a directory
|
|
82
|
+
* (e.g. AutoDL's /root/autodl-tmp) lists as a directory instead of 'other'.
|
|
83
|
+
* Symlinks are stat'd in PARALLEL batches: serializing them turns a conda /
|
|
84
|
+
* venv bin full of links into N round-trips (seconds to tens of seconds on a
|
|
85
|
+
* slow link) — batching keeps it to a handful of round-trips. The whole pass
|
|
86
|
+
* is bounded by ls()'s timeout.
|
|
87
|
+
*/
|
|
88
|
+
classifyEntries(sftp: import('ssh2').SFTPWrapper, dirPath: string, list: Array<{
|
|
89
|
+
filename: string;
|
|
90
|
+
attrs: import('ssh2').Stats;
|
|
91
|
+
}>): Promise<import('../protocol.ts').RemoteDirEntry[]>;
|
|
92
|
+
/** Stat one remote path (file browser / conflict checks). Bounded by a timeout. */
|
|
93
|
+
stat(alias: string, remotePath: string, signal?: AbortSignal): Promise<{
|
|
94
|
+
type: 'dir' | 'file' | 'other';
|
|
95
|
+
size: number;
|
|
96
|
+
mtimeMs: number;
|
|
97
|
+
mode: number;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Lstat one remote path without following the final symlink. Returns
|
|
101
|
+
* undefined when the path is absent (the fs seam's lstat contract).
|
|
102
|
+
*/
|
|
103
|
+
lstat(alias: string, remotePath: string, signal?: AbortSignal): Promise<{
|
|
104
|
+
type: 'file' | 'directory' | 'symlink' | 'other';
|
|
105
|
+
size: number;
|
|
106
|
+
mtimeMs: number;
|
|
107
|
+
mode: number;
|
|
108
|
+
} | undefined>;
|
|
109
|
+
/**
|
|
110
|
+
* Open a remote file read stream (the fs seam's streamText). The returned
|
|
111
|
+
* stream must be consumed or destroyed; the pooled connection stays busy
|
|
112
|
+
* for the stream's lifetime.
|
|
113
|
+
*/
|
|
114
|
+
/**
|
|
115
|
+
* Open a remote file read stream (the fs seam's streamText). The returned
|
|
116
|
+
* stream must be consumed or destroyed; the pooled connection stays busy
|
|
117
|
+
* for the stream's lifetime (P0-10: a 'stream' lease, released on
|
|
118
|
+
* end/close/error/destroy — not when this function returns).
|
|
119
|
+
*/
|
|
120
|
+
readStream(alias: string, remotePath: string, signal?: AbortSignal): Promise<import('node:stream').Readable>;
|
|
121
|
+
/**
|
|
122
|
+
* Read one remote file fully into memory (text or binary) with its mtime.
|
|
123
|
+
* The workspace plugin's text gate (UTF-8 + size caps) lives on its caller.
|
|
124
|
+
*/
|
|
125
|
+
readFile(alias: string, remotePath: string, maxBytes?: number, signal?: AbortSignal): Promise<{
|
|
126
|
+
content: Buffer;
|
|
127
|
+
mtime: number;
|
|
128
|
+
size: number;
|
|
129
|
+
}>;
|
|
130
|
+
/**
|
|
131
|
+
* Write one remote file from memory (parents are created). When
|
|
132
|
+
* `expectedMtime` is given, a stat-then-write conflict check throws before
|
|
133
|
+
* any byte is written (the GUI and the workspace tools use it for
|
|
134
|
+
* overwrite protection).
|
|
135
|
+
*/
|
|
136
|
+
writeFile(alias: string, remotePath: string, content: Buffer, expectedMtime?: number, signal?: AbortSignal): Promise<{
|
|
137
|
+
mtime: number;
|
|
138
|
+
}>;
|
|
139
|
+
/** Create a remote directory chain (mkdir -p semantics). */
|
|
140
|
+
mkdir(alias: string, remotePath: string, signal?: AbortSignal): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Remove a remote file or directory. Directories require `recursive: true`
|
|
143
|
+
* and are walked depth-first (children first, then the directory itself).
|
|
144
|
+
*
|
|
145
|
+
* Deletion never follows symlinks: every node is classified with lstat, so
|
|
146
|
+
* a symlink pointing at a directory is unlinked (only the link), never
|
|
147
|
+
* recursed into — the old stat/readdir-attr check could delete the link
|
|
148
|
+
* target's contents.
|
|
149
|
+
*/
|
|
150
|
+
rm(alias: string, remotePath: string, recursive?: boolean, signal?: AbortSignal): Promise<void>;
|
|
151
|
+
/** Rename / move a remote path (mv semantics, same filesystem). */
|
|
152
|
+
rename(alias: string, fromPath: string, toPath: string, signal?: AbortSignal): Promise<void>;
|
|
153
|
+
/** Reject a promise after `ms` (unref'd so it never keeps the process alive).
|
|
154
|
+
* `onTimeout` (when given) runs right before the rejection: ssh2 SFTP
|
|
155
|
+
* requests have no cancel API, so callers that hold an abort handle (e.g.
|
|
156
|
+
* a read stream) destroy it here — otherwise the underlying transfer would
|
|
157
|
+
* keep running (and, for reads, keep buffering) after the caller was told
|
|
158
|
+
* it timed out. */
|
|
159
|
+
withTimeout<T>(promise: Promise<T>, ms: number, message: string, onTimeout?: () => void): Promise<T>;
|
|
160
|
+
/** Bound one SFTP request and retire the subsystem only when it is safe.
|
|
161
|
+
*
|
|
162
|
+
* Every request rides ONE cached subsystem channel per pooled client. Closing
|
|
163
|
+
* that channel on a single request's timeout used to destroy every concurrent
|
|
164
|
+
* SFTP operation on the same connection. A timeout therefore only MARKS the
|
|
165
|
+
* channel suspect; the decision to close it belongs to the drain path, which
|
|
166
|
+
* ends it exactly once the last in-flight request has settled — so a stalled
|
|
167
|
+
* callback cannot linger behind another lease, and a healthy concurrent request
|
|
168
|
+
* is never destroyed. */
|
|
169
|
+
withSftpTimeout<T>(sftp: import('ssh2').SFTPWrapper, promise: Promise<T>, ms: number, message: string, onTimeout?: () => void): Promise<T>;
|
|
170
|
+
/** Count one operation against a cached subsystem's in-flight set. */
|
|
171
|
+
private enter;
|
|
172
|
+
/** End one operation's count; rotate a suspect channel only once it drains. */
|
|
173
|
+
private leave;
|
|
174
|
+
/**
|
|
175
|
+
* Retire a subsystem after a timeout without destroying concurrent SFTP work.
|
|
176
|
+
*
|
|
177
|
+
* A cached channel is SHARED by every operation on the pooled client, so
|
|
178
|
+
* closing it here would fail whatever else is in flight. Instead it is marked
|
|
179
|
+
* suspect and rotated by `leave()` once the last operation finishes. Only a
|
|
180
|
+
* wrapper this service does not track (a direct caller) is ended immediately.
|
|
181
|
+
*
|
|
182
|
+
* @param sftp - the subsystem that timed out.
|
|
183
|
+
*/
|
|
184
|
+
private retireSharedSubsystem;
|
|
185
|
+
/** The cache entry owning one resolved subsystem channel, if still cached. */
|
|
186
|
+
private entryFor;
|
|
187
|
+
/** Drop a suspect channel once it has drained, so the next call reopens. */
|
|
188
|
+
private rotateSuspect;
|
|
189
|
+
/** Stat wrapper (one SFTP stat call). */
|
|
190
|
+
sftpStat(sftp: import('ssh2').SFTPWrapper, remotePath: string): Promise<import('ssh2').Stats>;
|
|
191
|
+
/** Lstat wrapper (does NOT follow symlinks — the deletion safety gate). */
|
|
192
|
+
sftpLstat(sftp: import('ssh2').SFTPWrapper, remotePath: string, timeoutMs?: number): Promise<import('ssh2').Stats>;
|
|
193
|
+
/** Unlink wrapper with the same per-operation deadline as every other SFTP callback. */
|
|
194
|
+
sftpUnlink(sftp: import('ssh2').SFTPWrapper, remotePath: string, timeoutMs?: number): Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* The (cached) SFTP channel for a pooled client. `Client.sftp()` opens a new
|
|
197
|
+
* subsystem channel per call, so this memoizes one channel per live client;
|
|
198
|
+
* when the channel closes the cache entry is dropped so the next call opens
|
|
199
|
+
* SFTP on the replacement connection. Failed opens are also evicted so a
|
|
200
|
+
* transient channel failure can be retried.
|
|
201
|
+
*/
|
|
202
|
+
sftpFor(client: Client): Promise<import('ssh2').SFTPWrapper>;
|
|
203
|
+
/** Create a remote directory chain (stat-then-mkdir per segment). */
|
|
204
|
+
ensureRemoteDir(sftp: import('ssh2').SFTPWrapper, remote: string): Promise<void>;
|
|
205
|
+
fastPut(sftp: import('ssh2').SFTPWrapper, src: string, dst: string, onProgress?: (progress: TransferProgress) => void): Promise<void>;
|
|
206
|
+
fastGet(sftp: import('ssh2').SFTPWrapper, src: string, dst: string, initialTotal: number, onProgress?: (progress: TransferProgress) => void): Promise<void>;
|
|
207
|
+
/**
|
|
208
|
+
* Drop one client's cached channel. Wired as the connection pool's
|
|
209
|
+
* onDispose hook, so a torn-down client never keeps a half-open SFTP
|
|
210
|
+
* subsystem behind it.
|
|
211
|
+
*/
|
|
212
|
+
onClientDisposed(client: Client, error: Error): void;
|
|
213
|
+
/** Drop every cached channel (engine dispose). */
|
|
214
|
+
dispose(error: Error): void;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=service.d.ts.map
|
package/lib/types/ssh/store.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare class HostStore {
|
|
|
35
35
|
constructor(path?: string, sshConfigOverride?: string);
|
|
36
36
|
/** Load all entries (empty store when the file is absent). */
|
|
37
37
|
list(): SshHostEntry[];
|
|
38
|
+
/** The ssh-config path this store reads (override or the default). */
|
|
39
|
+
get sshConfigOverridePath(): string | undefined;
|
|
38
40
|
/** Find one entry by alias. */
|
|
39
41
|
find(alias: string): SshHostEntry | undefined;
|
|
40
42
|
/** Secret-free projection for the browser and agent surfaces. */
|
|
@@ -48,7 +50,8 @@ export declare class HostStore {
|
|
|
48
50
|
/**
|
|
49
51
|
* Import hosts from `~/.ssh/config`: Host blocks with a single non-wildcard
|
|
50
52
|
* pattern and a HostName become entries (key auth via IdentityFile, jump
|
|
51
|
-
* hosts via ProxyJump). Existing aliases are skipped.
|
|
53
|
+
* hosts via ProxyJump). Existing aliases are skipped. Each entry is created
|
|
54
|
+
* through this store's own `create`, which keeps plaintext secrets inline.
|
|
52
55
|
* @returns import statistics.
|
|
53
56
|
*/
|
|
54
57
|
importFromSshConfig(): ImportResult;
|
|
@@ -77,8 +80,9 @@ export interface ResolvedAuth {
|
|
|
77
80
|
*
|
|
78
81
|
* - **'none' (default, VSCode Remote-SSH style)**: secrets are NEVER
|
|
79
82
|
* persisted. create/update strip the password/passphrase from the entry
|
|
80
|
-
* (kind + keyPath stay); the engine prompts once
|
|
81
|
-
*
|
|
83
|
+
* (kind + keyPath stay); the engine prompts once and holds the credential
|
|
84
|
+
* in-memory for that CONNECTION's lifetime (the pool's retirement drops it).
|
|
85
|
+
* The plaintext HostStore
|
|
82
86
|
* remains untouched, so existing v1 tests and dual-format reads keep
|
|
83
87
|
* working.
|
|
84
88
|
*/
|
|
@@ -86,19 +90,27 @@ export declare class SecureHostStore {
|
|
|
86
90
|
private readonly vault;
|
|
87
91
|
private readonly mode;
|
|
88
92
|
private readonly inner;
|
|
93
|
+
private readonly pendingSecretCleanup;
|
|
89
94
|
constructor(vault: import('./vault.ts').Vault | undefined, path?: string, sshConfigOverride?: string, mode?: 'none' | 'vault');
|
|
90
95
|
list(): SshHostEntry[];
|
|
91
96
|
find(alias: string): SshHostEntry | undefined;
|
|
92
97
|
summarize(entry: SshHostEntry): SshHostSummary;
|
|
93
98
|
get path(): string;
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
/** The ssh-config path the underlying store reads (override or default). */
|
|
100
|
+
get sshConfigOverridePath(): string | undefined;
|
|
101
|
+
/** ssh-config import routed through THIS store's `create`, so every imported
|
|
102
|
+
* credential is handled per `secretStorage` (never persisted in 'none' mode,
|
|
103
|
+
* vault-encrypted in 'vault' mode) instead of being stored inline. */
|
|
104
|
+
importFromSshConfig(): Promise<ImportResult>;
|
|
105
|
+
/** Deferred best-effort cleanup queue (diagnostics/tests; contains no plaintext). */
|
|
106
|
+
pendingSecretCleanupRefs(): string[];
|
|
107
|
+
private removeSecret;
|
|
108
|
+
private retryPendingCleanup;
|
|
109
|
+
/** Create: secrets are staged, then rolled back if host persistence fails. */
|
|
98
110
|
create(payload: HostPayload): Promise<SshHostEntry>;
|
|
99
111
|
/** Update: auth present -> re-stash (or strip in none mode); absent → keep. */
|
|
100
112
|
update(alias: string, patch: Partial<HostPayload>): Promise<SshHostEntry>;
|
|
101
|
-
/** Delete
|
|
113
|
+
/** Delete the host first; vault cleanup can then only leave an orphan. */
|
|
102
114
|
delete(alias: string): Promise<void>;
|
|
103
115
|
/** Resolve the authentication for one entry (vault reveal when needed). */
|
|
104
116
|
resolveAuth(entry: SshHostEntry): Promise<ResolvedAuth>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Client } from 'ssh2';
|
|
2
|
+
import type { ExecSession, ShellSession } from '../engine.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Narrow connection dependency used by the standalone terminal transports.
|
|
5
|
+
* `connectStandalone` must reach the store itself (the not-found wording is
|
|
6
|
+
* this component's own contract) and must always return a PRIVATE connection
|
|
7
|
+
* — never a pooled one.
|
|
8
|
+
*/
|
|
9
|
+
export interface TerminalConnectionAccess {
|
|
10
|
+
connectStandalone(alias: string): Promise<{
|
|
11
|
+
client: Client;
|
|
12
|
+
hops: Client[];
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Owns the standalone PTY-shell / streaming-exec transports: one exclusive
|
|
17
|
+
* connection per session (closing a shell can therefore never tear down a
|
|
18
|
+
* pooled exec or tunnel sharing the alias), the channel-open deadline,
|
|
19
|
+
* idempotent teardown and session assembly.
|
|
20
|
+
*/
|
|
21
|
+
export declare class TerminalService {
|
|
22
|
+
private readonly connection;
|
|
23
|
+
/** Teardowns of live standalone transports, so dispose() can close sessions
|
|
24
|
+
* that the caller never closed explicitly (openShell/openExec own their
|
|
25
|
+
* whole connection and would otherwise outlive the engine). */
|
|
26
|
+
private readonly activeTeardowns;
|
|
27
|
+
constructor(connection: TerminalConnectionAccess);
|
|
28
|
+
/**
|
|
29
|
+
* Open one standalone channel on its own connection (never a pooled one).
|
|
30
|
+
* Shared by openShell/openExec: alias lookup, jump chain, channel-open
|
|
31
|
+
* timeout, idempotent teardown, and late-callback cleanup live here, so the
|
|
32
|
+
* two public methods keep only their session-specific assembly.
|
|
33
|
+
*/
|
|
34
|
+
private openStandaloneChannel;
|
|
35
|
+
/** Open a PTY shell session for the web terminal (standalone connection). */
|
|
36
|
+
openShell(alias: string, size: {
|
|
37
|
+
cols: number;
|
|
38
|
+
rows: number;
|
|
39
|
+
}): Promise<ShellSession>;
|
|
40
|
+
/**
|
|
41
|
+
* Open a streaming exec channel (no PTY) for the remote subprocess seam.
|
|
42
|
+
* Like the PTY shell, the channel rides its own connection so closing it
|
|
43
|
+
* can never tear down a pooled exec/tunnel sharing the alias.
|
|
44
|
+
*/
|
|
45
|
+
openExec(alias: string, command: string): Promise<ExecSession>;
|
|
46
|
+
/** Close every standalone transport this service still owns. */
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SshConnectionService } from '../connection/pool.ts';
|
|
2
|
+
import type { SshHostEntry, TunnelInfo } from '../protocol.ts';
|
|
3
|
+
/** Narrow connection dependency used by the tunnel component. */
|
|
4
|
+
export interface TunnelConnectionAccess {
|
|
5
|
+
readonly connections: SshConnectionService;
|
|
6
|
+
findEntry(alias: string): SshHostEntry | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Owns local listeners, forwarded sockets, and tunnel leases. */
|
|
9
|
+
export declare class TunnelService {
|
|
10
|
+
private readonly connection;
|
|
11
|
+
private readonly tunnels;
|
|
12
|
+
private nextTunnelId;
|
|
13
|
+
constructor(connection: TunnelConnectionAccess);
|
|
14
|
+
private removeClientFailureListener;
|
|
15
|
+
private markFailedForClient;
|
|
16
|
+
startTunnel(alias: string, options: {
|
|
17
|
+
remotePort: number;
|
|
18
|
+
remoteHost?: string;
|
|
19
|
+
localPort?: number;
|
|
20
|
+
}): Promise<TunnelInfo>;
|
|
21
|
+
listTunnels(): TunnelInfo[];
|
|
22
|
+
stopTunnel(id: string): boolean;
|
|
23
|
+
stopAllTunnels(alias?: string): number;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=service.d.ts.map
|
package/lib/types/ssh/vault.d.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module dsh-hardssh/vault
|
|
15
15
|
*/
|
|
16
|
+
/** Minimum non-whitespace master-password length. */
|
|
17
|
+
export declare const MIN_MASTER_PASSWORD_LENGTH = 8;
|
|
16
18
|
/** Types of secrets the vault stores. */
|
|
17
19
|
export type VaultPurpose = 'host.password' | 'host.passphrase';
|
|
18
20
|
/** KDF parameters persisted in the file header. */
|
|
@@ -29,8 +31,37 @@ export interface VaultStatus {
|
|
|
29
31
|
mode: 'env' | 'password';
|
|
30
32
|
entries: number;
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Directory holding the credential vault: `<home>/.dsh/ssh-secrets`.
|
|
36
|
+
*
|
|
37
|
+
* It IS inside `~/.dsh` (which the fs seam declares a local-infrastructure root
|
|
38
|
+
* so skills and plugin config stay readable), so its protection does NOT come
|
|
39
|
+
* from location — it comes from `SwitchFileSystem.deniedRoots` (fs.ts), which
|
|
40
|
+
* refuses this directory on every dispatch path, resolve to write, before any
|
|
41
|
+
* backend sees it.
|
|
42
|
+
*
|
|
43
|
+
* Residual risk, stated plainly: a command that runs on THIS machine as the
|
|
44
|
+
* same user (e.g. the client-side `pwsh` tool) can still read the file. What
|
|
45
|
+
* keeps the credential safe is that the file is encrypted (AES-256-GCM +
|
|
46
|
+
* scrypt) AND that environment auto-unlock is off by default — so a stolen
|
|
47
|
+
* ciphertext is only an offline scrypt target, not a usable credential.
|
|
48
|
+
*/
|
|
49
|
+
export declare function vaultDirectory(): string;
|
|
50
|
+
/** Vault file location: <home>/.dsh/ssh-secrets/dsh-ssh-vault.json. */
|
|
33
51
|
export declare function vaultPath(): string;
|
|
52
|
+
/** Pre-relocation vault location (<home>/.dsh/dsh-ssh-vault.json). */
|
|
53
|
+
export declare function legacyVaultPath(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Move a pre-relocation vault into {@link vaultDirectory} on first use.
|
|
56
|
+
*
|
|
57
|
+
* Never destructive: the new location wins when both exist, and a failed move
|
|
58
|
+
* falls back to copying so the source file is left in place.
|
|
59
|
+
*
|
|
60
|
+
* @param target - destination path (defaults to {@link vaultPath}).
|
|
61
|
+
* @param legacy - source path (defaults to {@link legacyVaultPath}).
|
|
62
|
+
* @returns true when a move/copy happened.
|
|
63
|
+
*/
|
|
64
|
+
export declare function migrateLegacyVault(target?: string, legacy?: string): boolean;
|
|
34
65
|
/** SHA3-256 hex of a UTF-8 string. */
|
|
35
66
|
export declare function sha3_256Hex(text: string): string;
|
|
36
67
|
/** Errors raised by the vault. */
|
|
@@ -64,19 +95,31 @@ export interface RevealedSecret {
|
|
|
64
95
|
*/
|
|
65
96
|
export declare class Vault {
|
|
66
97
|
private readonly path;
|
|
67
|
-
private readonly passwordProvider;
|
|
68
98
|
private document;
|
|
69
99
|
private unlockedKey;
|
|
70
100
|
private lockTimer;
|
|
101
|
+
private readonly idleLockMs;
|
|
71
102
|
private readonly leaked;
|
|
103
|
+
/** Whether `DSH_CREDENTIAL_PASSWORD` may unlock this instance at load. */
|
|
104
|
+
private readonly allowEnvUnlock;
|
|
105
|
+
/** Single-instance mutation queue. Every operation that can change the
|
|
106
|
+
* in-memory document or persist it runs in invocation order. The tail is
|
|
107
|
+
* always recovered so one rejected mutation never poisons later work. */
|
|
108
|
+
private mutationTail;
|
|
72
109
|
constructor(filePath?: string, options?: {
|
|
73
|
-
/** Session password source (loopback unlock route passes a password). */
|
|
74
|
-
passwordProvider?: () => Promise<string | undefined>;
|
|
75
110
|
/** Vault idle auto-lock (ms); 0 disables (default 30 min). */
|
|
76
111
|
idleLockMs?: number;
|
|
112
|
+
/**
|
|
113
|
+
* Allow unlocking from `DSH_CREDENTIAL_PASSWORD` at construction.
|
|
114
|
+
* OFF by default: that variable is visible to anything running as the
|
|
115
|
+
* same user — including an agent session — so auto-unlock is an explicit
|
|
116
|
+
* opt-in (`vaultAutoUnlock: 'env'` in the plugin config), not a default.
|
|
117
|
+
*/
|
|
118
|
+
allowEnvUnlock?: boolean;
|
|
77
119
|
});
|
|
78
120
|
private load;
|
|
79
121
|
private save;
|
|
122
|
+
private saveDocument;
|
|
80
123
|
private deriveKey;
|
|
81
124
|
private lockoutRetryAfter;
|
|
82
125
|
private checkLockout;
|
|
@@ -84,6 +127,10 @@ export declare class Vault {
|
|
|
84
127
|
private encryptEntryBuffer;
|
|
85
128
|
private decryptEntryBuffer;
|
|
86
129
|
private requireKey;
|
|
130
|
+
private validateMasterPassword;
|
|
131
|
+
/** Successful credential activity extends the idle lock deadline. */
|
|
132
|
+
private touch;
|
|
133
|
+
private enqueueMutation;
|
|
87
134
|
private tryEnvUnlock;
|
|
88
135
|
/** Current status (locked / mode / entry count) — safe for UIs. */
|
|
89
136
|
status(): VaultStatus;
|
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `ctx.subprocess` switch row: provides the
|
|
3
|
-
* facade in the host scope. The local runtime is mounted in an
|
|
4
|
-
* child scope;
|
|
5
|
-
*
|
|
6
|
-
* its cwd. The facade auto-provides `subprocess` here because the plain
|
|
7
|
-
* subprocess row is disabled by the profile patch.
|
|
2
|
+
* The `ctx.subprocess` switch row: provides the generic WorkspaceCore-routing
|
|
3
|
+
* subprocess facade in the host scope. The local runtime is mounted in an
|
|
4
|
+
* isolated child scope; each spawn resolves by cwd through the same workspace
|
|
5
|
+
* router used by the filesystem seam.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* synchronously on every commit, so creating or removing an SSH workspace
|
|
13
|
-
* takes effect without a plugin restart and the fs/subprocess seams can never
|
|
14
|
-
* disagree. Before the initial ledger load finishes, routing degrades to
|
|
15
|
-
* local with a one-time warning for anchor-root paths.
|
|
7
|
+
* A bound connection supplies `workspace.process`. Before the core is ready,
|
|
8
|
+
* or for an unowned cwd beneath the managed SSH anchor root, spawning fails
|
|
9
|
+
* closed rather than executing on the client machine.
|
|
16
10
|
*
|
|
17
11
|
* @module dsh-hardssh/subprocess
|
|
18
12
|
*/
|
|
19
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
20
|
-
import type {
|
|
21
|
-
|
|
22
|
-
interface Context {
|
|
23
|
-
hardsshCore: HardsshCore;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
14
|
+
import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
|
|
15
|
+
import type { WorkspaceCore } from './runtime/workspace-core.ts';
|
|
26
16
|
/** Stable cordis plugin name. */
|
|
27
17
|
export declare const name = "hardssh-subprocess";
|
|
28
|
-
/**
|
|
18
|
+
/** Workspace routing is exclusively provided by the generic core. */
|
|
29
19
|
export declare const inject: string[];
|
|
30
|
-
/**
|
|
20
|
+
/** Resolve the subprocess runtime owning a cwd, or undefined when local. */
|
|
21
|
+
export declare function genericSubprocessFor(core: WorkspaceCore, cwd: string | undefined, reservedAnchorRoots: readonly string[]): SubprocessRuntime | undefined;
|
|
22
|
+
/** Mount the generic switching subprocess facade. */
|
|
31
23
|
export declare function apply(ctx: Context): void;
|
|
32
24
|
//# sourceMappingURL=subprocess.d.ts.map
|
|
@@ -46,20 +46,70 @@ export interface SwitchFsDeps {
|
|
|
46
46
|
/** The world owning one namespaced target key (never undefined: every
|
|
47
47
|
* key this facade issued maps back). */
|
|
48
48
|
worldForNamespace(namespace: string): WorkspaceWorld | undefined;
|
|
49
|
+
/** Client-side roots that must stay LOCAL even in a bound workspace (dsh's
|
|
50
|
+
* own `~/.dsh` with skills & harness state, plugin configs, …). Default
|
|
51
|
+
* routing in a remote world is REMOTE for everything else. */
|
|
52
|
+
localRoots?: ReadonlyArray<string>;
|
|
53
|
+
/** Windows inside `localRoots` that must NEVER be treated as local
|
|
54
|
+
* infrastructure — the managed workspace ANCHOR ROOT. Anchors physically
|
|
55
|
+
* live under `~/.dsh`, so without this carve-out a path inside the anchors
|
|
56
|
+
* window (including a stale anchor dir with no ledger record) would fall
|
|
57
|
+
* into the local exception and a remote session would silently write to
|
|
58
|
+
* the client machine. Semantics: local exception = localRoots MINUS
|
|
59
|
+
* exclusions. */
|
|
60
|
+
localRootExclusions?: ReadonlyArray<string>;
|
|
61
|
+
/** Roots whose contents are NEVER accessible through this seam — the
|
|
62
|
+
* encrypted credential store. Enforced on every dispatch path (see
|
|
63
|
+
* `assertNotDenied`), so a bound session's agent cannot read the vault
|
|
64
|
+
* ciphertext through the routed filesystem and attack it offline. It does
|
|
65
|
+
* NOT constrain a command that runs natively on this machine as the same
|
|
66
|
+
* user (e.g. the client-side `pwsh` tool). */
|
|
67
|
+
deniedRoots?: ReadonlyArray<string>;
|
|
68
|
+
/** The world owning an ABSOLUTE workspace anchor path (the caller's own
|
|
69
|
+
* workspace or a SIBLING workspace's anchor), or undefined when no
|
|
70
|
+
* workspace claims that path. Consulted BEFORE the `localRoots`
|
|
71
|
+
* carve-out: managed SSH workspace anchors live under `~/.dsh` (a
|
|
72
|
+
* declared local root), so routing a sibling anchor to local would
|
|
73
|
+
* silently run a remote-workspace file operation on this machine. */
|
|
74
|
+
worldForAnchorPath?(path: string): WorkspaceWorld | undefined;
|
|
49
75
|
}
|
|
50
76
|
/** The routing filesystem facade. */
|
|
51
77
|
export declare class SwitchFileSystem extends FileSystem {
|
|
52
78
|
private readonly deps;
|
|
53
79
|
private readonly local;
|
|
54
80
|
constructor(ctx: Context, deps: SwitchFsDeps);
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
81
|
+
/** The deployment's local backend is still a sandboxed filesystem, and
|
|
82
|
+
* `dsh-tool-fs` reads this capability fact ONCE at apply() to decide whether
|
|
83
|
+
* to advertise `sandbox_permissions` / `justification` at all. Returning
|
|
84
|
+
* undefined here disabled that escalation entry for EVERY session,
|
|
85
|
+
* including purely local ones, so the local backend's mode is the honest
|
|
86
|
+
* answer. Remote worlds are not confined by the local sandbox, so a wider
|
|
87
|
+
* per-call policy is meaningless for them and is dropped at the call site
|
|
88
|
+
* instead of being silently swallowed by a 4-parameter remote backend. */
|
|
57
89
|
get sandboxMode(): SandboxMode | undefined;
|
|
58
90
|
/** Encode a raw key into the world's namespace. */
|
|
59
91
|
private encode;
|
|
60
92
|
/** Decode a namespaced key back to (rawKey, world). Supports both the
|
|
61
93
|
* current `wfs://<id>/…` form and the legacy `ssh:<recordId>:…` form. */
|
|
62
94
|
private decode;
|
|
95
|
+
/**
|
|
96
|
+
* Refuse a protected client directory before any backend sees it.
|
|
97
|
+
*
|
|
98
|
+
* Only a LOCAL world is checked: for a remote world the path is a path on the
|
|
99
|
+
* SERVER, which may legitimately look like a client path (both can be
|
|
100
|
+
* `/home/me/.dsh/…`).
|
|
101
|
+
*
|
|
102
|
+
* @param path - the path (or canonical target key) about to be dispatched.
|
|
103
|
+
* @param namespace - the resolved world's namespace ('' = local).
|
|
104
|
+
*/
|
|
105
|
+
private assertNotDenied;
|
|
106
|
+
/**
|
|
107
|
+
* The world owning one call: an explicit anchor path (this workspace's or a
|
|
108
|
+
* SIBLING's) wins over the session cwd, because anchors are physically under
|
|
109
|
+
* the local-infrastructure root and would otherwise be carved out as local.
|
|
110
|
+
* Falls back to the session cwd's world, then to the caller's default.
|
|
111
|
+
*/
|
|
112
|
+
private worldForPath;
|
|
63
113
|
resolve(path: string, opts?: {
|
|
64
114
|
cwd?: string;
|
|
65
115
|
signal?: AbortSignal;
|