@tiphareth/dsh-hardssh 0.1.2 → 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 +1171 -1321
  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 +58 -25
  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 -96
  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 -225
  173. package/src/remote-runner.ts +0 -201
  174. package/src/seam-state.ts +0 -185
@@ -0,0 +1,19 @@
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
+
6
+ import type { FileSystem } from '@deepseek-ai/dsh-fs'
7
+ import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess'
8
+ import type { WorkspaceSearchService } from '../base/capability.ts'
9
+
10
+ /** DSH's official workspace capability bindings; no terminal key exists because process owns terminals. */
11
+ export interface DshWorkspaceCapabilityMap {
12
+ 'workspace.fs': FileSystem
13
+ 'workspace.process': SubprocessRuntime
14
+ 'workspace.search': WorkspaceSearchService
15
+ }
16
+
17
+ declare module '../base/model.ts' {
18
+ interface WorkspaceCapabilityMap extends DshWorkspaceCapabilityMap {}
19
+ }
@@ -1,48 +1,63 @@
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
 
15
10
  import type { Context } from '@deepseek-ai/cordis'
11
+ import { randomUUID } from 'node:crypto'
16
12
  import { homedir } from 'node:os'
17
13
  import { join } from 'node:path'
18
- import { WorkspaceProviderRegistry, type WorkspaceRegistry } from '../base/registry.ts'
19
- import type { WorkspaceRecord } from '../base/model.ts'
14
+ import type { LedgerListener, WorkspaceLedger as WorkspaceLedgerType } from '../base/ledger.ts'
20
15
  import { WorkspaceLedger } from '../base/ledger.ts'
16
+ import { LedgerWorkspaceRouter } from '../base/ledger-router.ts'
17
+ import type {
18
+ WorkspaceConnection,
19
+ WorkspaceCreateInput,
20
+ WorkspaceId,
21
+ WorkspaceProvider,
22
+ WorkspaceRecord,
23
+ WorkspaceUpdate,
24
+ } from '../base/model.ts'
25
+ import { WorkspaceProviderRegistry, type WorkspaceRegistry } from '../base/registry.ts'
21
26
  import { registerBuiltinProviders } from '../providers/index.ts'
22
- import type { SshEngine } from '../ssh/engine.ts'
23
27
  import type { HostStoreView } from '../core.ts'
24
- import type { SshWorkspaceLedger } from '../ledger.ts'
25
- import type { SshWorkspaceRecord } from '../protocol.ts'
26
- import { LedgerWorkspaceRouter } from '../base/ledger-router.ts'
27
- import type { WorkspaceProvider } from '../base/model.ts'
28
+ import type { SshEngine } from '../ssh/engine.ts'
29
+ import './dsh-capabilities.ts'
28
30
 
29
- /** The canonical workspace-core service shape. */
31
+ export type WorkspaceListener = LedgerListener
32
+
33
+ /** Canonical high-level workspace consumer surface. */
30
34
  export interface WorkspaceCore {
31
- /** Generic ledger (WorkspaceRecord CRUD + anchor index). */
32
- ledger: WorkspaceLedger
33
- /** Provider registry (local/ssh/builtin + third-party). */
34
- providers: WorkspaceProviderRegistry
35
- /** Ledger+provider router for the switch facades. */
36
- router: LedgerWorkspaceRouter
37
- /** Registry surface exposed to plugins. */
38
- registry: WorkspaceRegistry
39
- /** Map a legacy SSH record into a generic WorkspaceRecord. */
40
- fromSshRecord(record: SshWorkspaceRecord): WorkspaceRecord
35
+ initialize(): Promise<void>
36
+ isReady(): boolean
37
+ whenReady(): Promise<void>
38
+ list(): Promise<WorkspaceRecord[]>
39
+ get(id: WorkspaceId): Promise<WorkspaceRecord | undefined>
40
+ findByAnchor(path: string): Promise<WorkspaceRecord | undefined>
41
+ openById(id: WorkspaceId, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>
42
+ openByAnchor(path: string, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>
43
+ create(input: WorkspaceCreateInput): Promise<WorkspaceRecord>
44
+ update(id: WorkspaceId, patch: WorkspaceUpdate): Promise<WorkspaceRecord | undefined>
45
+ remove(id: WorkspaceId): Promise<boolean>
46
+ subscribe(listener: WorkspaceListener): () => void
47
+ registerProvider(provider: WorkspaceProvider): () => void
48
+ closeAll(): Promise<void>
49
+
50
+ /** @deprecated Internal compatibility surface; ordinary consumers use high-level methods. */
51
+ readonly ledger: WorkspaceLedger
52
+ /** @deprecated Internal compatibility surface; ordinary consumers use registerProvider(). */
53
+ readonly providers: WorkspaceProviderRegistry
54
+ /** @deprecated Internal compatibility surface; ordinary consumers use openById/openByAnchor(). */
55
+ readonly router: LedgerWorkspaceRouter
56
+ /** @deprecated Plugin compatibility surface. */
57
+ readonly registry: WorkspaceRegistry
41
58
  }
42
59
 
43
- /**
44
- * A WorkspaceRegistry backed by the generic ledger + provider registry.
45
- */
60
+ /** Ledger-backed plugin surface retained while current plugins migrate to WorkspaceCore. */
46
61
  class LedgerBasedWorkspaceRegistry implements WorkspaceRegistry {
47
62
  constructor(
48
63
  private readonly ledger: WorkspaceLedger,
@@ -54,11 +69,17 @@ class LedgerBasedWorkspaceRegistry implements WorkspaceRegistry {
54
69
  }
55
70
 
56
71
  subscribe(listener: (workspace: WorkspaceRecord) => void): () => void {
57
- return this.ledger.subscribe((change) => listener(change.record))
72
+ return this.ledger.subscribe(change => {
73
+ if (change.type === 'replaced') {
74
+ for (const workspace of change.records) listener(workspace)
75
+ } else {
76
+ listener(change.record)
77
+ }
78
+ })
58
79
  }
59
80
 
60
- register(provider: WorkspaceProvider): void {
61
- this.providerRegistry.register(provider)
81
+ register(provider: WorkspaceProvider): () => void {
82
+ return this.providerRegistry.register(provider)
62
83
  }
63
84
 
64
85
  unregister(providerId: string): void {
@@ -74,27 +95,108 @@ class LedgerBasedWorkspaceRegistry implements WorkspaceRegistry {
74
95
  }
75
96
  }
76
97
 
77
- /** Convert a legacy SSH-bound workspace record into the generic model. */
78
- export function sshRecordToWorkspaceRecord(record: SshWorkspaceRecord): WorkspaceRecord {
79
- return {
80
- schemaVersion: 1,
81
- id: record.id,
82
- title: record.title,
83
- provider: {
84
- id: 'ssh',
85
- connectionRef: { id: record.alias, alias: record.alias },
86
- },
87
- location: {
88
- kind: 'posix',
89
- root: record.remoteRoot,
90
- },
91
- anchor: {
92
- path: record.anchorPath,
93
- mode: 'managed',
94
- },
95
- createdAt: record.createdAt,
96
- updatedAt: record.createdAt,
97
- extensions: {},
98
+ /** mountWorkspaceCore() requires Cordis and builtins, so this class makes the same core testable with injected generic foundations. */
99
+ export class DefaultWorkspaceCore implements WorkspaceCore {
100
+ readonly registry: WorkspaceRegistry
101
+
102
+ constructor(
103
+ readonly ledger: WorkspaceLedgerType,
104
+ readonly providers: WorkspaceProviderRegistry,
105
+ readonly router = new LedgerWorkspaceRouter(ledger, providers),
106
+ ) {
107
+ this.registry = new LedgerBasedWorkspaceRegistry(ledger, providers)
108
+ }
109
+
110
+ /** WorkspaceLedger.load() cannot preopen providers or establish router readiness, so initialize delegates to router initialization. */
111
+ initialize(): Promise<void> {
112
+ return this.router.initialize()
113
+ }
114
+
115
+ /** WorkspaceLedger.snapshotSync() cannot prove provider preopen completed, so readiness comes from the router lifecycle. */
116
+ isReady(): boolean {
117
+ return this.router.isReady()
118
+ }
119
+
120
+ /** isReady() cannot await a pending or failed initialization, so whenReady delegates to the router promise. */
121
+ whenReady(): Promise<void> {
122
+ return this.router.whenReady()
123
+ }
124
+
125
+ /** WorkspaceLedger.list() alone may expose records before runtime readiness, so list waits for initialization first. */
126
+ async list(): Promise<WorkspaceRecord[]> {
127
+ await this.whenReady()
128
+ return this.ledger.list()
129
+ }
130
+
131
+ /** WorkspaceLedger.get() alone may run before strict load/preopen, so get waits for initialization first. */
132
+ async get(id: WorkspaceId): Promise<WorkspaceRecord | undefined> {
133
+ await this.whenReady()
134
+ return this.ledger.get(id)
135
+ }
136
+
137
+ /** WorkspaceLedger.findByAnchor() alone may run before strict load/preopen, so findByAnchor waits for initialization first. */
138
+ async findByAnchor(path: string): Promise<WorkspaceRecord | undefined> {
139
+ await this.whenReady()
140
+ return this.ledger.findByAnchor(path)
141
+ }
142
+
143
+ /** LedgerWorkspaceRouter.ensureOpen() requires a record, so openById exposes identity-based opening to consumers. */
144
+ async openById(id: WorkspaceId, signal?: AbortSignal): Promise<WorkspaceConnection | undefined> {
145
+ await this.whenReady()
146
+ return this.router.openById(id, signal)
147
+ }
148
+
149
+ /** WorkspaceLedger.findByAnchor() returns data only, so openByAnchor exposes the resolved connection in one operation. */
150
+ async openByAnchor(path: string, signal?: AbortSignal): Promise<WorkspaceConnection | undefined> {
151
+ await this.whenReady()
152
+ return this.router.openByAnchor(path, signal)
153
+ }
154
+
155
+ /** WorkspaceLedger.create() cannot validate provider-specific references, so create validates before committing the record. */
156
+ async create(input: WorkspaceCreateInput): Promise<WorkspaceRecord> {
157
+ await this.whenReady()
158
+ const provider = this.providers.get(input.provider.id)
159
+ if (provider === undefined) throw new Error(`workspace.provider-missing: '${input.provider.id}'`)
160
+ const timestamp = new Date().toISOString()
161
+ await provider.validate({ ...structuredClone(input), id: input.id ?? randomUUID(), createdAt: timestamp, updatedAt: timestamp })
162
+ const record = await this.ledger.create(input)
163
+ await this.router.openById(record.id)
164
+ return record
165
+ }
166
+
167
+ /** WorkspaceLedger.update() cannot validate a changed provider binding, so update checks the proposed record before commit. */
168
+ async update(id: WorkspaceId, patch: WorkspaceUpdate): Promise<WorkspaceRecord | undefined> {
169
+ await this.whenReady()
170
+ const before = await this.ledger.get(id)
171
+ if (before === undefined) return undefined
172
+ const proposed: WorkspaceRecord = { ...before, ...structuredClone(patch), id: before.id, schemaVersion: before.schemaVersion, createdAt: before.createdAt }
173
+ const provider = this.providers.get(proposed.provider.id)
174
+ if (provider === undefined) throw new Error(`workspace.provider-missing: '${proposed.provider.id}'`)
175
+ await provider.validate(proposed)
176
+ const record = await this.ledger.update(id, patch)
177
+ if (record !== undefined) await this.router.openById(id)
178
+ return record
179
+ }
180
+
181
+ /** WorkspaceLedger.remove() owns persistence while its synchronous change event makes router removal fail closed before this promise returns. */
182
+ async remove(id: WorkspaceId): Promise<boolean> {
183
+ await this.whenReady()
184
+ return this.ledger.remove(id)
185
+ }
186
+
187
+ /** WorkspaceLedger.list() cannot publish later mutations, so subscribe exposes its detached commit stream. */
188
+ subscribe(listener: WorkspaceListener): () => void {
189
+ return this.ledger.subscribe(listener)
190
+ }
191
+
192
+ /** WorkspaceProviderRegistry.get() cannot establish disposer ownership, so registerProvider returns the registry-owned disposer. */
193
+ registerProvider(provider: WorkspaceProvider): () => void {
194
+ return this.providers.register(provider)
195
+ }
196
+
197
+ /** WorkspaceConnection.close() only closes one handle, so closeAll delegates complete cached/in-flight teardown to the router. */
198
+ closeAll(): Promise<void> {
199
+ return this.router.closeAll()
98
200
  }
99
201
  }
100
202
 
@@ -108,47 +210,28 @@ export function genericAnchorRoot(): string {
108
210
  return join(homedir(), '.dsh', 'workspaces', 'anchors')
109
211
  }
110
212
 
111
- /**
112
- * Build and provide the workspace core on the cordis context: registers the
113
- * builtin providers, instantiates the generic ledger (persisting under
114
- * ~/.dsh/workspaces/index.v1.json), constructs the ledger router, and
115
- * provides `workspaceCore`.
116
- */
213
+ /** mountWorkspaceCore() cannot register Cordis services or builtin providers, so it performs host assembly (provider registration + `ctx.provide('workspaceCore')`); the boot calls `initialize()` afterwards. */
117
214
  export function mountWorkspaceCore(ctx: Context, deps: { engine?: SshEngine; hosts?: HostStoreView }): WorkspaceCore {
118
215
  const providers = new WorkspaceProviderRegistry()
119
216
  const ledger = new WorkspaceLedger(genericLedgerPath(), genericAnchorRoot())
120
-
121
- // Register builtin providers against the shared registry.
122
- registerBuiltinProviders(providers, deps)
123
-
124
- const registry: WorkspaceRegistry = new LedgerBasedWorkspaceRegistry(ledger, providers)
125
- const router = new LedgerWorkspaceRouter(ledger, registry, {
126
- open: async (record) => {
127
- const provider = providers.get(record.provider.id)
128
- if (provider === undefined) return undefined
129
- const connection = await provider.open(record)
130
- return connection
131
- },
132
- onRemoved: () => { /* connection cleanup handled by closeAll */ },
133
- })
134
-
135
- const core: WorkspaceCore = {
136
- ledger,
137
- providers,
138
- router,
139
- registry,
140
- fromSshRecord: sshRecordToWorkspaceRecord,
141
- }
217
+ // The SSH/local providers build real DSH FileSystem / SubprocessRuntime
218
+ // capabilities on isolated per-workspace scopes, so they require this host
219
+ // ctx (mandatory — there is no capability fallback path any more).
220
+ const providerDisposers = registerBuiltinProviders(providers, deps, ctx)
221
+ const core = new DefaultWorkspaceCore(ledger, providers)
142
222
 
143
223
  ctx.provide('workspaceCore', core)
224
+ // Unload must release every opened connection and provider registration this
225
+ // assembly created.
226
+ ctx.effect(() => async () => {
227
+ await core.closeAll()
228
+ for (const dispose of providerDisposers) dispose()
229
+ }, 'dsh-hardssh: generic workspace core')
144
230
  return core
145
231
  }
146
232
 
147
- /** The legacy SSH ledger held by the hardssh core (type bridge). */
148
- export type { SshWorkspaceLedger }
149
-
150
233
  declare module '@deepseek-ai/cordis' {
151
234
  interface Context {
152
235
  workspaceCore: WorkspaceCore
153
236
  }
154
- }
237
+ }