@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
@@ -13,10 +13,11 @@
13
13
  *
14
14
  * @module @tiphareth/dsh-hardssh/providers/ssh
15
15
  */
16
+ import type { Context } from '@deepseek-ai/cordis';
16
17
  import type { WorkspaceCapabilityMap, WorkspaceConnection, WorkspaceProvider, WorkspaceProviderManifest } from '../../base/model.ts';
17
- import type { WorkspaceFileSystem, WorkspaceProcessRuntime, WorkspaceSearchService, WorkspaceStat, WorkspaceTerminalService, WorkspaceDirEntry, WorkspaceSearchHit } from '../../base/capability.ts';
18
+ import type { WorkspaceSearchHit, WorkspaceSearchService } from '../../base/capability.ts';
18
19
  import type { SshEngine } from '../../ssh/engine.ts';
19
- /** The ssh provider manifest. */
20
+ /** The ssh provider manifest (provider API v2, no separate terminal capability — terminals live on `workspace.process`). */
20
21
  export declare const sshProviderManifest: WorkspaceProviderManifest;
21
22
  /** One open SSH workspace connection. */
22
23
  export declare class SshWorkspaceConnection implements WorkspaceConnection {
@@ -24,73 +25,35 @@ export declare class SshWorkspaceConnection implements WorkspaceConnection {
24
25
  private readonly engine;
25
26
  private readonly alias;
26
27
  private readonly remoteRoot;
28
+ private readonly cordisContext;
27
29
  readonly providerId = "ssh";
28
- constructor(workspaceId: string, engine: SshEngine, alias: string, remoteRoot: string);
30
+ private state;
31
+ private fsInstance;
32
+ private processInstance;
33
+ private searchInstance;
34
+ /** One workspace is one fixed remote execution world for every capability instance. */
35
+ private readonly stateOf;
36
+ constructor(workspaceId: string, engine: SshEngine, alias: string, remoteRoot: string, cordisContext: Context);
29
37
  get<K extends keyof WorkspaceCapabilityMap>(capability: K): WorkspaceCapabilityMap[K] | undefined;
30
38
  private fs;
31
39
  private process;
32
- private terminal;
33
40
  private search;
34
41
  status(): 'connecting' | 'ready' | 'degraded' | 'closed';
35
42
  close(): Promise<void>;
36
43
  }
37
44
  /** POSIX-join a remote root with a relative path, confining to the root. */
38
45
  export declare function joinRemoteRoot(root: string, path: string): string;
39
- /** Wrap engine FS calls as the generic WorkspaceFileSystem. */
40
- export declare class SshWorkspaceFileSystem implements WorkspaceFileSystem {
41
- private readonly engine;
42
- private readonly alias;
43
- private readonly root;
44
- constructor(engine: SshEngine, alias: string, root: string);
45
- private full;
46
- stat(path: string, signal?: AbortSignal): Promise<WorkspaceStat | undefined>;
47
- list(path: string, signal?: AbortSignal): Promise<WorkspaceDirEntry[]>;
48
- readFile(path: string, signal?: AbortSignal): Promise<Uint8Array>;
49
- writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void>;
50
- mkdir(path: string, options?: {
51
- recursive?: boolean;
52
- signal?: AbortSignal;
53
- }): Promise<void>;
54
- rm(path: string, options?: {
55
- recursive?: boolean;
56
- signal?: AbortSignal;
57
- }): Promise<void>;
58
- rename(from: string, to: string, signal?: AbortSignal): Promise<void>;
59
- }
60
- /** Wrap engine exec as the generic WorkspaceProcessRuntime. */
61
- export declare class SshWorkspaceProcess implements WorkspaceProcessRuntime {
62
- private readonly engine;
63
- private readonly alias;
64
- private readonly root;
65
- constructor(engine: SshEngine, alias: string, root: string);
66
- exec(command: string, options?: {
67
- timeoutMs?: number;
68
- cwd?: string;
69
- signal?: AbortSignal;
70
- }): Promise<{
71
- stdout: string;
72
- stderr: string;
73
- exitCode: number;
74
- timedOut?: boolean;
75
- durationMs?: number;
76
- }>;
77
- private full;
78
- }
79
- /** Wrap engine PTY as the generic WorkspaceTerminalService. */
80
- export declare class SshWorkspaceTerminal implements WorkspaceTerminalService {
81
- private readonly engine;
82
- private readonly alias;
83
- private readonly root;
84
- constructor(engine: SshEngine, alias: string, root: string);
85
- openTerminal(cols: number, rows: number): Promise<import('../../base/capability.ts').WorkspaceTerminalHandle>;
86
- }
87
- /** Wrap the remote search service as the generic WorkspaceSearchService. */
46
+ /** Wrap the shared RemoteSearchService as the generic WorkspaceSearchService. */
88
47
  export declare class SshWorkspaceSearch implements WorkspaceSearchService {
89
48
  private readonly engine;
90
49
  private readonly alias;
91
- private readonly root;
50
+ private readonly remoteRoot;
92
51
  private readonly service;
93
- constructor(engine: SshEngine, alias: string, root: string);
52
+ constructor(engine: SshEngine, alias: string, remoteRoot: string);
53
+ /** Workspace-relative POSIX search base → absolute remote root. */
54
+ private searchBase;
55
+ /** Absolute path → path relative to `base`, or undefined when outside it. */
56
+ private relativeTo;
94
57
  glob(pattern: string, options?: {
95
58
  root?: string;
96
59
  maxDepth?: number;
@@ -106,7 +69,11 @@ export declare class SshWorkspaceSearch implements WorkspaceSearchService {
106
69
  hits: WorkspaceSearchHit[];
107
70
  truncated: boolean;
108
71
  }>;
72
+ /** Depth of a base-relative path (root-level entries are depth 1, find -maxdepth style). */
73
+ private depthOf;
109
74
  }
110
- /** The ssh provider factory. */
111
- export declare function createSshWorkspaceProvider(engine: SshEngine): WorkspaceProvider;
75
+ /** The ssh provider factory. `context` is mandatory: the provider serves only
76
+ * the real DSH FileSystem/SubprocessRuntime capabilities, which need a Cordis
77
+ * scope to isolate each workspace's resources. */
78
+ export declare function createSshWorkspaceProvider(engine: SshEngine, context: Context): WorkspaceProvider;
112
79
  //# sourceMappingURL=provider.d.ts.map
@@ -24,6 +24,8 @@ export declare function invalidateRemoteEnvironment(engine: SshEngine, alias: st
24
24
  export declare function scrubRemoteEnvironment(environment: ReadonlyMap<string, string>): Map<string, string>;
25
25
  /**
26
26
  * Overlay explicit entries and serialize one validated environment for `env -i`.
27
+ * Login-critical names are emitted first (see CRITICAL_ENV_ORDER); everything
28
+ * else keeps the remote's own order.
27
29
  */
28
30
  export declare function serializeEnvironment(scrubbed: ReadonlyMap<string, string>, explicit: Readonly<NodeJS.ProcessEnv> | undefined): string;
29
31
  //# sourceMappingURL=environment.d.ts.map
@@ -14,6 +14,8 @@ import type { SshEngine } from '../ssh/engine.ts';
14
14
  import { FileSystem, FsVersion } from '@deepseek-ai/dsh-fs';
15
15
  import type { FsDirEntry, FsEditOutcome, FsEditRequest, FsInfo, FsPathInfo, FsTarget, FsWriteIntent, FsWriteOutcome } from '@deepseek-ai/dsh-fs';
16
16
  import type { WorkspaceState } from '../protocol.ts';
17
+ /** Decode a base64-wrapped NUL-terminated canonical path from `realpath -mz`. */
18
+ export declare function decodeCanonicalPath(encoded: string): string;
17
19
  /**
18
20
  * Remote filesystem backend over the dsh-ssh engine. The working directory
19
21
  * follows the mode store: relative paths resolve against the resolved remote
@@ -24,7 +26,15 @@ export declare class SshFileSystem extends FileSystem {
24
26
  private readonly engine;
25
27
  private readonly getState;
26
28
  private readonly locks;
27
- constructor(ctx: Context, engine: SshEngine, getState: () => WorkspaceState);
29
+ /** When set, every resolved target must stay under this root (the SSH
30
+ * workspace's `location.root`). Absent for the legacy/global SSH seam,
31
+ * which intentionally addresses the whole host. */
32
+ private readonly confineRoot;
33
+ constructor(ctx: Context, engine: SshEngine, getState: () => WorkspaceState, confineRoot?: string);
34
+ /** Root-confinement gate for the workspace capability: reject any canonical
35
+ * path that escapes `confineRoot` (including via an in-root symlink whose
36
+ * realpath lands outside). Mirrors the local provider's fail-closed rule. */
37
+ private confine;
28
38
  /** The active remote execution world (throws when not in remote mode). */
29
39
  private current;
30
40
  /**
@@ -53,6 +63,7 @@ export declare class SshFileSystem extends FileSystem {
53
63
  version: ReturnType<typeof FsVersion>;
54
64
  }, signal?: AbortSignal): Promise<FsEditOutcome>;
55
65
  private withLock;
66
+ private canonicalTarget;
56
67
  private canonicalPath;
57
68
  private probe;
58
69
  private requireRegular;
@@ -22,7 +22,13 @@ export declare class SshSubprocessHandle implements SubprocessHandle {
22
22
  private readonly stdoutCollector;
23
23
  private readonly stderrCollector;
24
24
  private session;
25
+ private inputSink;
26
+ private inputErrorListener;
27
+ private terminationPhase;
25
28
  private graceTimer;
29
+ private forceTimer;
30
+ private resolveForced;
31
+ private forceClosed;
26
32
  private settled;
27
33
  constructor(engine: SshEngine, getState: () => WorkspaceState, spec: SubprocessSpawnSpec, spillDir: string);
28
34
  /** Remote process id; `-1` because the SSH channel does not expose one. */
@@ -32,9 +38,24 @@ export declare class SshSubprocessHandle implements SubprocessHandle {
32
38
  /** @inheritdoc */
33
39
  waitForExit(signal?: AbortSignal): Promise<boolean>;
34
40
  private readonly onAbort;
35
- private signalTerm;
41
+ private signalCurrentPhase;
42
+ private startTermination;
43
+ /** Immediately close the owned channel and settle `done`, including while
44
+ * openExec is still pending. A late-opened channel observes forceClosed and
45
+ * closes itself before any stream wiring is installed. */
46
+ forceClose(): void;
36
47
  private settle;
37
48
  private run;
49
+ /** Install every callback under one ownership boundary. Any synchronous
50
+ * wiring failure or async stdin sink failure closes the established session
51
+ * and rejects the handle instead of leaking it. */
52
+ private runSession;
53
+ /** Route one stream through the engine's leak guard before it reaches the
54
+ * consumer. Without this, a remote `bash`/subprocess channel bypassed the
55
+ * redaction that every exec/cluster result already applies. Redaction is
56
+ * exact-match over known secrets, so it is best-effort and may change byte
57
+ * length — the same trade-off the PTY route documents. */
58
+ private redactBytes;
38
59
  private wireStdout;
39
60
  private wireStderr;
40
61
  }
@@ -11,6 +11,9 @@ import type { SshEngine } from '../ssh/engine.ts';
11
11
  import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
12
12
  import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
13
13
  import type { WorkspaceState } from '../protocol.ts';
14
+ /** Hard upper bound for plugin/workspace teardown, independent of a caller's
15
+ * potentially very large per-process TERM/KILL grace setting. */
16
+ export declare const SUBPROCESS_DISPOSE_DEADLINE_MS = 5000;
14
17
  /** SSH command manager registered as `ctx.subprocess` (remote mode). */
15
18
  export declare class SshSubprocessRuntime extends SubprocessRuntime {
16
19
  private readonly engine;
@@ -18,8 +21,17 @@ export declare class SshSubprocessRuntime extends SubprocessRuntime {
18
21
  private readonly live;
19
22
  private readonly terminals;
20
23
  private readonly spillDir;
21
- private disposing;
24
+ private closePromise;
22
25
  constructor(ctx: Context, engine: SshEngine, getState: () => WorkspaceState);
26
+ /**
27
+ * Public idempotent close that releases every workspace-owned live process
28
+ * and terminal. The ctx.effect teardown cannot be invoked by a workspace
29
+ * connection (it only runs when the cordis scope disposes, which would also
30
+ * tear down the shared engine's scope), so a SshWorkspaceConnection calls
31
+ * this on-demand close() instead — the engine pool itself stays untouched.
32
+ */
33
+ close(): Promise<void>;
34
+ private closeOwnedResources;
23
35
  /** @inheritdoc */
24
36
  resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
25
37
  /** @inheritdoc */
@@ -30,6 +30,10 @@ export declare class SshTerminalHandle implements SubprocessTerminalHandle {
30
30
  signalForeground(_signal: SubprocessTerminalSignal): Promise<number>;
31
31
  /** @inheritdoc */
32
32
  terminate(): Promise<void>;
33
+ /** Immediately release the channel when the owning runtime's independent
34
+ * disposal deadline expires. This also settles output/done if ssh2 never
35
+ * reports a terminal exit after close(). */
36
+ forceClose(): void;
33
37
  private signal;
34
38
  private closeOnce;
35
39
  }
@@ -41,10 +41,10 @@ export declare class RemoteSearchService {
41
41
  private readonly engine;
42
42
  constructor(engine: SshEngine);
43
43
  /** Literal file-name search under `root` (skips node_modules/.git). */
44
- searchNames(target: SearchTarget, query: string): Promise<RemoteNameSearch>;
44
+ searchNames(target: SearchTarget, query: string, signal?: AbortSignal): Promise<RemoteNameSearch>;
45
45
  /** Glob search (pattern keeps glob semantics; `**` crosses directories). */
46
- glob(target: SearchTarget, pattern: string): Promise<RemoteGlobResult>;
46
+ glob(target: SearchTarget, pattern: string, signal?: AbortSignal): Promise<RemoteGlobResult>;
47
47
  /** Fixed-string grep (literal pattern, `-F`; NUL file boundary `-Z`). */
48
- grepFixed(target: SearchTarget, pattern: string): Promise<RemoteGrepResult>;
48
+ grepFixed(target: SearchTarget, pattern: string, signal?: AbortSignal): Promise<RemoteGrepResult>;
49
49
  }
50
50
  //# sourceMappingURL=remote-search.d.ts.map
@@ -1,24 +1,52 @@
1
1
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
2
2
  import { type SshEngine } from './ssh/engine.ts';
3
- import type { SshWorkspaceLedger } from './ledger.ts';
4
- import { type WorkspaceFileService } from './backend.ts';
3
+ import type { WorkspaceStoreView } from './backend.ts';
4
+ /** One record whose host-workspace (re)registration failed. */
5
+ export interface HostWorkspaceReconcileFailure {
6
+ id: string;
7
+ error: string;
8
+ }
9
+ /** Result of replaying host-workspace registration for every stored record. */
10
+ export interface HostWorkspaceReconcileReport {
11
+ registered: number;
12
+ failures: HostWorkspaceReconcileFailure[];
13
+ /** Set when the record source itself could not be listed (startup path
14
+ * tolerates it; the route surfaces it as an I/O failure). */
15
+ listError?: string;
16
+ }
17
+ /** Registration replay dependencies (the route and the startup path share it). */
18
+ export interface HostWorkspaceReconcileDeps {
19
+ workspaces: WorkspaceStoreView;
20
+ registerHostWorkspace?: (anchorPath: string, title: string) => Promise<void>;
21
+ }
22
+ /**
23
+ * Replay host-workspace registration for every stored record. Registration
24
+ * happens only on create/delete, so a process restart (or a registration that
25
+ * failed before compensation existed) leaves records whose sidebar entry is
26
+ * missing; `registerHostWorkspace` is create-if-missing, so this is the
27
+ * idempotent startup/retry compensation. Per-record failures are reported and
28
+ * never thrown: a broken host registry must not break plugin startup.
29
+ *
30
+ * Shared by the `/reconcile` route and the boot path so the two halves cannot
31
+ * drift.
32
+ */
33
+ export declare function reconcileHostWorkspaces(deps: HostWorkspaceReconcileDeps): Promise<HostWorkspaceReconcileReport>;
5
34
  /** Route family dependencies. */
6
35
  export interface WorkspaceRoutesDeps {
7
36
  /** Read-only host surface (list only; the SSH routes own the write path). */
8
37
  hosts: import('./core.ts').HostStoreView;
9
38
  engine: SshEngine;
10
- ledger: SshWorkspaceLedger;
11
- /** Gated workspace file service (tree / file / search). */
12
- files: WorkspaceFileService;
39
+ /** SSH-workspace record source: the generic WorkspaceCore-backed
40
+ * projection store (the only runtime). */
41
+ workspaces: WorkspaceStoreView;
13
42
  /** Register the anchor dir as a HOST workspace (sidebar visibility). */
14
43
  registerHostWorkspace?: (anchorPath: string, title: string) => Promise<void>;
15
44
  /** Drop the host workspace registration for an anchor dir. */
16
45
  unregisterHostWorkspace?: (anchorPath: string) => Promise<void>;
17
46
  }
18
47
  /**
19
- * Build every /api/dsh-hardssh route.
20
- * @param deps - host store (alias listing), ssh engine, workspace ledger, file service.
21
- * @returns the routes to register.
48
+ * Build every /api/dsh-hardssh route (workspace CRUD + remote directory
49
+ * browsing for the picker; the per-workspace file surface was removed).
22
50
  */
23
51
  export declare function makeRoutes(deps: WorkspaceRoutesDeps): WebRoute[];
24
52
  //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The sole DSH runtime augmentation point for generic workspace capabilities.
3
+ * The platform-neutral model cannot name these runtime services itself.
4
+ */
5
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
6
+ import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
7
+ import type { WorkspaceSearchService } from '../base/capability.ts';
8
+ /** DSH's official workspace capability bindings; no terminal key exists because process owns terminals. */
9
+ export interface DshWorkspaceCapabilityMap {
10
+ 'workspace.fs': FileSystem;
11
+ 'workspace.process': SubprocessRuntime;
12
+ 'workspace.search': WorkspaceSearchService;
13
+ }
14
+ declare module '../base/model.ts' {
15
+ interface WorkspaceCapabilityMap extends DshWorkspaceCapabilityMap {
16
+ }
17
+ }
18
+ //# sourceMappingURL=dsh-capabilities.d.ts.map
@@ -1,56 +1,91 @@
1
1
  /**
2
- * Runtime bridge: exposes the generic workspace base (ledger, provider
3
- * registry, router, plugin host) as a cordis service under the canonical
4
- * `workspaceCore` name, while keeping the legacy `hardsshCore` /
5
- * `sshWorkspaceCore` aliases pointing at the same instances so existing
6
- * consumers keep working.
7
- *
8
- * This module is the DSH boundary: it imports the base (provider-agnostic)
9
- * and the legacy SSH modules, but the base itself never imports this
10
- * module — direction of dependency stays base ← runtime.
11
- *
12
- * @module @tiphareth/dsh-hardssh/runtime/workspace-core
2
+ * High-level generic WorkspaceCore runtime boundary. The generic core is the
3
+ * ONLY production workspace runtime: it owns the ledger, the record CRUD, and
4
+ * the provider routing used by the fs/subprocess seams, the workspace routes,
5
+ * the `remote_*` agent tools, and the SSH host-delete guard. Mounting is
6
+ * side-effect-free until `initialize()` runs, which the plugin boot does
7
+ * explicitly.
13
8
  */
14
9
  import type { Context } from '@deepseek-ai/cordis';
15
- import { WorkspaceProviderRegistry, type WorkspaceRegistry } from '../base/registry.ts';
16
- import type { WorkspaceRecord } from '../base/model.ts';
10
+ import type { LedgerListener, WorkspaceLedger as WorkspaceLedgerType } from '../base/ledger.ts';
17
11
  import { WorkspaceLedger } from '../base/ledger.ts';
18
- import type { SshEngine } from '../ssh/engine.ts';
19
- import type { HostStoreView } from '../core.ts';
20
- import type { SshWorkspaceLedger } from '../ledger.ts';
21
- import type { SshWorkspaceRecord } from '../protocol.ts';
22
12
  import { LedgerWorkspaceRouter } from '../base/ledger-router.ts';
23
- /** The canonical workspace-core service shape. */
13
+ import type { WorkspaceConnection, WorkspaceCreateInput, WorkspaceId, WorkspaceProvider, WorkspaceRecord, WorkspaceUpdate } from '../base/model.ts';
14
+ import { WorkspaceProviderRegistry, type WorkspaceRegistry } from '../base/registry.ts';
15
+ import type { HostStoreView } from '../core.ts';
16
+ import type { SshEngine } from '../ssh/engine.ts';
17
+ import './dsh-capabilities.ts';
18
+ export type WorkspaceListener = LedgerListener;
19
+ /** Canonical high-level workspace consumer surface. */
24
20
  export interface WorkspaceCore {
25
- /** Generic ledger (WorkspaceRecord CRUD + anchor index). */
26
- ledger: WorkspaceLedger;
27
- /** Provider registry (local/ssh/builtin + third-party). */
28
- providers: WorkspaceProviderRegistry;
29
- /** Ledger+provider router for the switch facades. */
30
- router: LedgerWorkspaceRouter;
31
- /** Registry surface exposed to plugins. */
32
- registry: WorkspaceRegistry;
33
- /** Map a legacy SSH record into a generic WorkspaceRecord. */
34
- fromSshRecord(record: SshWorkspaceRecord): WorkspaceRecord;
21
+ initialize(): Promise<void>;
22
+ isReady(): boolean;
23
+ whenReady(): Promise<void>;
24
+ list(): Promise<WorkspaceRecord[]>;
25
+ get(id: WorkspaceId): Promise<WorkspaceRecord | undefined>;
26
+ findByAnchor(path: string): Promise<WorkspaceRecord | undefined>;
27
+ openById(id: WorkspaceId, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
28
+ openByAnchor(path: string, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
29
+ create(input: WorkspaceCreateInput): Promise<WorkspaceRecord>;
30
+ update(id: WorkspaceId, patch: WorkspaceUpdate): Promise<WorkspaceRecord | undefined>;
31
+ remove(id: WorkspaceId): Promise<boolean>;
32
+ subscribe(listener: WorkspaceListener): () => void;
33
+ registerProvider(provider: WorkspaceProvider): () => void;
34
+ closeAll(): Promise<void>;
35
+ /** @deprecated Internal compatibility surface; ordinary consumers use high-level methods. */
36
+ readonly ledger: WorkspaceLedger;
37
+ /** @deprecated Internal compatibility surface; ordinary consumers use registerProvider(). */
38
+ readonly providers: WorkspaceProviderRegistry;
39
+ /** @deprecated Internal compatibility surface; ordinary consumers use openById/openByAnchor(). */
40
+ readonly router: LedgerWorkspaceRouter;
41
+ /** @deprecated Plugin compatibility surface. */
42
+ readonly registry: WorkspaceRegistry;
43
+ }
44
+ /** mountWorkspaceCore() requires Cordis and builtins, so this class makes the same core testable with injected generic foundations. */
45
+ export declare class DefaultWorkspaceCore implements WorkspaceCore {
46
+ readonly ledger: WorkspaceLedgerType;
47
+ readonly providers: WorkspaceProviderRegistry;
48
+ readonly router: LedgerWorkspaceRouter;
49
+ readonly registry: WorkspaceRegistry;
50
+ constructor(ledger: WorkspaceLedgerType, providers: WorkspaceProviderRegistry, router?: LedgerWorkspaceRouter);
51
+ /** WorkspaceLedger.load() cannot preopen providers or establish router readiness, so initialize delegates to router initialization. */
52
+ initialize(): Promise<void>;
53
+ /** WorkspaceLedger.snapshotSync() cannot prove provider preopen completed, so readiness comes from the router lifecycle. */
54
+ isReady(): boolean;
55
+ /** isReady() cannot await a pending or failed initialization, so whenReady delegates to the router promise. */
56
+ whenReady(): Promise<void>;
57
+ /** WorkspaceLedger.list() alone may expose records before runtime readiness, so list waits for initialization first. */
58
+ list(): Promise<WorkspaceRecord[]>;
59
+ /** WorkspaceLedger.get() alone may run before strict load/preopen, so get waits for initialization first. */
60
+ get(id: WorkspaceId): Promise<WorkspaceRecord | undefined>;
61
+ /** WorkspaceLedger.findByAnchor() alone may run before strict load/preopen, so findByAnchor waits for initialization first. */
62
+ findByAnchor(path: string): Promise<WorkspaceRecord | undefined>;
63
+ /** LedgerWorkspaceRouter.ensureOpen() requires a record, so openById exposes identity-based opening to consumers. */
64
+ openById(id: WorkspaceId, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
65
+ /** WorkspaceLedger.findByAnchor() returns data only, so openByAnchor exposes the resolved connection in one operation. */
66
+ openByAnchor(path: string, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
67
+ /** WorkspaceLedger.create() cannot validate provider-specific references, so create validates before committing the record. */
68
+ create(input: WorkspaceCreateInput): Promise<WorkspaceRecord>;
69
+ /** WorkspaceLedger.update() cannot validate a changed provider binding, so update checks the proposed record before commit. */
70
+ update(id: WorkspaceId, patch: WorkspaceUpdate): Promise<WorkspaceRecord | undefined>;
71
+ /** WorkspaceLedger.remove() owns persistence while its synchronous change event makes router removal fail closed before this promise returns. */
72
+ remove(id: WorkspaceId): Promise<boolean>;
73
+ /** WorkspaceLedger.list() cannot publish later mutations, so subscribe exposes its detached commit stream. */
74
+ subscribe(listener: WorkspaceListener): () => void;
75
+ /** WorkspaceProviderRegistry.get() cannot establish disposer ownership, so registerProvider returns the registry-owned disposer. */
76
+ registerProvider(provider: WorkspaceProvider): () => void;
77
+ /** WorkspaceConnection.close() only closes one handle, so closeAll delegates complete cached/in-flight teardown to the router. */
78
+ closeAll(): Promise<void>;
35
79
  }
36
- /** Convert a legacy SSH-bound workspace record into the generic model. */
37
- export declare function sshRecordToWorkspaceRecord(record: SshWorkspaceRecord): WorkspaceRecord;
38
80
  /** Persistence path for the generic ledger (~/.dsh/workspaces/index.v1.json). */
39
81
  export declare function genericLedgerPath(): string;
40
82
  /** Anchor root for managed generic workspaces (~/.dsh/workspaces/anchors). */
41
83
  export declare function genericAnchorRoot(): string;
42
- /**
43
- * Build and provide the workspace core on the cordis context: registers the
44
- * builtin providers, instantiates the generic ledger (persisting under
45
- * ~/.dsh/workspaces/index.v1.json), constructs the ledger router, and
46
- * provides `workspaceCore`.
47
- */
84
+ /** mountWorkspaceCore() cannot register Cordis services or builtin providers, so it performs host assembly (provider registration + `ctx.provide('workspaceCore')`); the boot calls `initialize()` afterwards. */
48
85
  export declare function mountWorkspaceCore(ctx: Context, deps: {
49
86
  engine?: SshEngine;
50
87
  hosts?: HostStoreView;
51
88
  }): WorkspaceCore;
52
- /** The legacy SSH ledger held by the hardssh core (type bridge). */
53
- export type { SshWorkspaceLedger };
54
89
  declare module '@deepseek-ai/cordis' {
55
90
  interface Context {
56
91
  workspaceCore: WorkspaceCore;
@@ -0,0 +1,113 @@
1
+ /** Phase 3 legacy-to-generic conversion and side-effect-free shadow comparison. */
2
+ import { type WorkspaceNamespaceCodec } from '../base/namespace.ts';
3
+ import type { WorkspaceRecord } from '../base/model.ts';
4
+ import type { SshWorkspaceRecord } from '../protocol.ts';
5
+ /**
6
+ * Which runtime the migration is preparing. The legacy runtime is gone — the
7
+ * generic core is the ONLY production runtime — so this has a single value; it
8
+ * stays a named field so the cutover marker and the migration report record
9
+ * which runtime wrote them.
10
+ */
11
+ export type WorkspaceRuntimeMode = 'generic';
12
+ export interface WorkspaceMigrationOptions {
13
+ mode: WorkspaceRuntimeMode;
14
+ legacyPath: string;
15
+ genericPath: string;
16
+ reportPath?: string;
17
+ now?: () => Date;
18
+ }
19
+ export interface WorkspaceMigrationReport {
20
+ schemaVersion: 1;
21
+ mode: WorkspaceRuntimeMode;
22
+ createdAt: string;
23
+ status: 'migrated' | 'unchanged';
24
+ sourceDigest: string;
25
+ targetDigest: string;
26
+ recordCount: number;
27
+ ids: string[];
28
+ backupPath?: string;
29
+ differences: string[];
30
+ }
31
+ export interface ShadowCapabilityObservation {
32
+ providerId: string;
33
+ manifestCapabilities: readonly string[];
34
+ availableCapabilities: readonly string[];
35
+ }
36
+ export interface WorkspaceShadowComparisonInput {
37
+ legacy: readonly SshWorkspaceRecord[];
38
+ generic: readonly WorkspaceRecord[];
39
+ anchorProbes?: readonly string[];
40
+ namespaceProbes?: readonly string[];
41
+ codec?: WorkspaceNamespaceCodec;
42
+ capabilities?: readonly ShadowCapabilityObservation[];
43
+ }
44
+ export interface WorkspaceShadowComparisonReport {
45
+ matches: boolean;
46
+ differences: string[];
47
+ }
48
+ /** The generic ledger's create() generates identity and time fields, so it cannot preserve a legacy record exactly; this mapper exists to retain the source id/anchor/timestamp verbatim. */
49
+ export declare function legacySshRecordToWorkspaceRecord(record: SshWorkspaceRecord): WorkspaceRecord;
50
+ /** Repeated WorkspaceLedger.create() calls cannot provide an all-or-nothing full-snapshot conversion with one backup. */
51
+ export declare function migrateLegacySshLedger(options: WorkspaceMigrationOptions): Promise<WorkspaceMigrationReport>;
52
+ /** LedgerWorkspaceRouter.fromAnchor() cannot compare detached legacy/generic decisions, and capability get() must not run in shadow. */
53
+ export declare function compareWorkspaceShadow(input: WorkspaceShadowComparisonInput): WorkspaceShadowComparisonReport;
54
+ /** Where a recovered generic ledger came from. */
55
+ export type GenericLedgerRecoverySource = 'last-good' | 'backup' | 'legacy';
56
+ /** Outcome of one missing-ledger recovery attempt. */
57
+ export interface GenericLedgerRecoveryResult {
58
+ recovered: boolean;
59
+ source?: GenericLedgerRecoverySource;
60
+ /** The file the records were restored from (absent for a legacy re-import). */
61
+ path?: string;
62
+ /** How many records the restored ledger holds. */
63
+ recordCount?: number;
64
+ /** Why recovery failed, when it did. */
65
+ reason?: string;
66
+ }
67
+ /** Options for {@link recoverGenericLedger}. */
68
+ export interface GenericLedgerRecoveryOptions {
69
+ /** The missing/unreadable generic ledger target. */
70
+ genericPath: string;
71
+ /** The frozen legacy SSH ledger, the last-resort source. */
72
+ legacyPath: string;
73
+ /**
74
+ * Record ids the cutover marker proves existed. A candidate that cannot
75
+ * produce ALL of them is rejected: recovery must never silently restore a
76
+ * SMALLER workspace set than the marker recorded — that would look like a
77
+ * successful recovery while quietly losing workspaces. An empty/absent list
78
+ * disables the id check (count is still required to be non-zero).
79
+ */
80
+ expectedIds?: readonly string[];
81
+ /** Sink for recovery diagnostics (defaults to console.warn). */
82
+ log?: (message: string) => void;
83
+ }
84
+ /**
85
+ * Classify the generic ledger file so the boot can tell "legitimately empty"
86
+ * (the user deleted every workspace, the file exists with `[]`) from "lost or
87
+ * damaged" (missing, truncated, or unparseable).
88
+ *
89
+ * @param path - the generic ledger path.
90
+ * @returns the ledger state, never throwing.
91
+ */
92
+ export declare function inspectGenericLedger(path: string): Promise<'absent' | 'readable' | 'corrupt'>;
93
+ /**
94
+ * Restore a generic ledger that the cutover marker proves should exist.
95
+ *
96
+ * The marker records that a migration committed N workspaces, so a missing or
97
+ * unreadable ledger is data loss — never a fresh deployment. Recovery is
98
+ * attempted newest-first: the rolling `.last-good` snapshot, then the newest
99
+ * `.backup-<timestamp>` left by a ledger replacement, then a re-import from the
100
+ * frozen legacy source. EVERY candidate must satisfy `expectedIds`; a source
101
+ * that would restore fewer workspaces than the marker recorded is rejected
102
+ * rather than accepted as success.
103
+ *
104
+ * The legacy tier deliberately migrates WITHOUT a report path: rewriting the
105
+ * marker there could downgrade it (an empty legacy source produces
106
+ * `recordCount: 0`) and permanently disarm the gate that asked for this
107
+ * recovery in the first place.
108
+ *
109
+ * @param options - paths, the expected ids, and an optional logger.
110
+ * @returns which source was used, or `recovered: false` plus the reason.
111
+ */
112
+ export declare function recoverGenericLedger(options: GenericLedgerRecoveryOptions): Promise<GenericLedgerRecoveryResult>;
113
+ //# sourceMappingURL=workspace-migration.d.ts.map
@@ -8,9 +8,32 @@ export interface ClientLease {
8
8
  readonly generation: number;
9
9
  readonly kind: LeaseKind;
10
10
  readonly released: boolean;
11
+ /**
12
+ * True when this is the only lease on its pooled connection.
13
+ *
14
+ * Cancellation uses it to decide the blast radius: retiring the transport is
15
+ * safe (nobody else is using it) while other holders exist it would destroy
16
+ * unrelated work on the same alias.
17
+ */
18
+ holdsOnlyLease(): boolean;
11
19
  /** Mark the underlying connection broken (surfaces to every holder). */
12
20
  markBroken(error?: unknown): void;
13
21
  /** Idempotent: release this holder's ownership of the connection. */
14
22
  release(): void;
15
23
  }
24
+ /**
25
+ * Tie a lease's release to the OPERATION's settlement, not to the caller's
26
+ * promise.
27
+ *
28
+ * Why this exists: `withClient` rejects its caller as soon as an abort arrives,
29
+ * but the underlying request may keep running (SFTP has no cancel API). If the
30
+ * lease were released at rejection time, `holdsOnlyLease()` would report "no
31
+ * other user" while that request was still in flight — and a later abort in a
32
+ * different session would then close the shared transport underneath it.
33
+ *
34
+ * @param lease - the lease owned by this operation.
35
+ * @param operation - the operation whose settlement frees the lease.
36
+ * @returns the operation, unchanged, with the release chained to it.
37
+ */
38
+ export declare function holdLeaseUntilSettled<T>(lease: ClientLease, operation: Promise<T>): Promise<T>;
16
39
  //# sourceMappingURL=lease.d.ts.map