@tiphareth/dsh-hardssh 0.1.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +77 -50
  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 +333 -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 +42 -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 +59 -26
  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 +171 -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
package/lib/types/fs.d.ts CHANGED
@@ -1,36 +1,53 @@
1
1
  /**
2
- * The `ctx.fs` switch row: provides the workspace-routing filesystem facade in
3
- * the host scope. The local backend (the deployment's sandboxed filesystem) is
4
- * mounted in an isolated child scope so its own `ctx.fs` provide never
5
- * collides. Routing and per-record remote backends are owned by the SHARED
6
- * seam state (hardsshCore.seams): the row only binds its per-record backend
7
- * factory and reads the state on every call. Every SSH-bound workspace record
8
- * gets its OWN remote backend instance bound to that record's alias + remote
9
- * root; the facade routes each call by the session cwd — an anchor path of an
10
- * SSH workspace routes remote, everything else stays local. The facade
11
- * auto-provides `fs` here because the `fs-sandbox` row is disabled by the
12
- * profile patch.
2
+ * The `ctx.fs` switch row: provides the generic WorkspaceCore-routing
3
+ * filesystem facade in the host scope. The local backend (the deployment's
4
+ * sandboxed filesystem) is mounted in an isolated child scope so its own
5
+ * `ctx.fs` provide never collides.
13
6
  *
14
- * The seam state re-applies the ledger snapshot synchronously on every
15
- * commit, so creating or removing an SSH workspace takes effect without a
16
- * plugin restart and the fs/subprocess seams can never disagree. Before the
17
- * initial ledger load finishes, routing degrades to local with a one-time
18
- * warning for anchor-root paths.
7
+ * Routing worlds always come from `WorkspaceCore`/router:
8
+ * `connection.get('workspace.fs')` returns the root-bound DSH `FileSystem`
9
+ * cached by the provider. A cwd inside a workspace anchor resolves through
10
+ * that connection; everything else stays local. Before the core is ready, or
11
+ * for an unowned path beneath the managed SSH anchor root, access fails closed
12
+ * rather than touching the client filesystem.
19
13
  *
20
14
  * @module dsh-hardssh/fs
21
15
  */
22
16
  import type { Context } from '@deepseek-ai/cordis';
23
- import type { HardsshCore } from './core.ts';
24
- declare module '@deepseek-ai/cordis' {
25
- interface Context {
26
- hardsshCore: HardsshCore;
27
- }
28
- }
17
+ import { type WorkspaceWorld } from './switch/switch-fs.ts';
18
+ import type { WorkspaceCore } from './runtime/workspace-core.ts';
29
19
  /** Stable cordis plugin name. */
30
20
  export declare const name = "hardssh-fs";
31
- /** Services required: the shared workspace core (mode store + engine) and the
32
- * sandbox policy the local backend (`SandboxedFileSystem`) consumes. */
21
+ /** The generic workspace core plus the policy used by the local fallback. */
33
22
  export declare const inject: string[];
34
- /** Mount the switching filesystem facade. */
23
+ /** Resolve the fs world owning a cwd, or undefined when it is definitely local. */
24
+ export declare function genericFsWorldFor(core: WorkspaceCore, cwd: string | undefined, reservedAnchorRoots: readonly string[]): WorkspaceWorld | undefined;
25
+ /** Resolve one namespaced target key, failing closed for stale namespaces. */
26
+ export declare function genericFsWorldForNamespace(core: WorkspaceCore, namespace: string): WorkspaceWorld | undefined;
27
+ /**
28
+ * Refuse a path that sits inside the managed anchor root but is owned by no
29
+ * registered workspace — as a NOT-FOUND answer, not as a fatal error.
30
+ *
31
+ * The path stays unreachable either way (it is never handed to the local
32
+ * backend, so a bound session cannot read or write client files through the
33
+ * anchor window). What changes is how callers SEE the refusal: `FS_NOT_FOUND`
34
+ * is the DSH contract for "this path does not exist", and the harness relies on
35
+ * it when walking UP from the session cwd looking for a project root —
36
+ * `dsh-agent-instructions` probes `<dir>/.git` per ancestor and treats ONLY
37
+ * `FS_NOT_FOUND` as "keep walking" (any other error aborts the whole run),
38
+ * `dsh-skill-filesystem` does the same. With a bare Error the very first step
39
+ * above the anchor (`<anchorRoot>/.git`) killed the run with
40
+ * "fs-ssh: … is inside the workspace anchor root but no registered workspace
41
+ * owns it (fail closed)". The message is kept verbatim so the refusal is still
42
+ * self-explanatory in logs.
43
+ */
44
+ export declare function refuseUnownedAnchorPath(path: string): never;
45
+ /**
46
+ * The shipped `worldForAnchorPath` deps hook: the workspace owning an absolute
47
+ * anchor path (this session's or a SIBLING's), else the anchor-window refusal.
48
+ * Exported so the seam can be exercised without re-implementing the policy.
49
+ */
50
+ export declare function anchorWorldFor(core: WorkspaceCore, anchorRootDir: string, path: string): WorkspaceWorld | undefined;
51
+ /** Mount the generic switching filesystem facade. */
35
52
  export declare function apply(ctx: Context): void;
36
53
  //# sourceMappingURL=fs.d.ts.map
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * dsh-hardssh — host half. Owns the local⇄remote mode store, the
3
3
  * /api/dsh-hardssh route family (loopback-only), the remote_* agent
4
- * tools, a model-facing announcement section, the shared workspace core
5
- * (`ctx.hardsshCore`, including the shared seam state the fs/subprocess
6
- * switch rows consume), and — since the legacy dsh-ssh package was merged
4
+ * tools, a model-facing announcement section, the generic workspace core,
5
+ * and since the legacy dsh-ssh package was merged
7
6
  * in — the SSH operations capability (host manager, ssh_* tools, /api/dsh-ssh,
8
7
  * web terminal). In SSH mode the model's ordinary read/write/edit/bash tools
9
8
  * run transparently on the remote host through those switch rows. File
@@ -13,14 +12,25 @@
13
12
  * the header buttons, the SSH config dialog, the left workspace panel, and
14
13
  * the SSH host-manager surfaces.
15
14
  *
16
- * The announcement section is rendered PER SESSION from the ledger facts
15
+ * The announcement section is rendered PER SESSION from the workspace facts
17
16
  * (which workspace this session's cwd binds to), never from path-string
18
- * heuristics; and a global tool guard blocks glob/grep/pwsh calls in
19
- * SSH-bound sessions with a clear error instead of letting them fail with
20
- * "No such file or directory" on the remote host.
17
+ * heuristics. There is no tool guard layer: routing lives in the fs/subprocess
18
+ * seams, and the subprocess facade refuses the one combination that cannot be
19
+ * routed correctly a client-side search helper (glob/grep's bundled ripgrep)
20
+ * inside a remote-bound session, which would otherwise return confidently wrong
21
+ * "no matches" from the local anchor.
22
+ *
23
+ * The generic WorkspaceCore is the only production workspace runtime. On the
24
+ * first upgraded boot, the frozen legacy SSH ledger is imported once into
25
+ * `~/.dsh/workspaces/index.v1.json`; the atomic migration report is the durable
26
+ * cutover marker. Emergency generic-to-legacy conversion is an offline export,
27
+ * never an alternate in-process routing mode.
21
28
  */
22
29
  import type { Context } from '@deepseek-ai/cordis';
23
30
  import z from '@deepseek-ai/schemastery';
31
+ import { type HostWorkspaceReconcileDeps, type HostWorkspaceReconcileReport } from './routes.ts';
32
+ import { type WorkspaceCore } from './runtime/workspace-core.ts';
33
+ import type { SshWorkspaceRecord } from './protocol.ts';
24
34
  /** Stable cordis plugin name. */
25
35
  export declare const name = "hardssh";
26
36
  /**
@@ -32,18 +42,137 @@ export declare const name = "hardssh";
32
42
  export declare const inject: string[];
33
43
  /** Plugin config (schemastery; optional fields use .default, never .optional). */
34
44
  export interface Config {
35
- /** Master switch (default on). Disabling requires reverting the profile seam patch. */
45
+ /**
46
+ * Workspace-surface switch (default on). It gates ONLY the SSH-workspace
47
+ * surfaces this row mounts after the check below: the /api/dsh-hardssh
48
+ * workspace CRUD routes, the remote_* workspace agent tools, and the
49
+ * workspace announcement section. It does NOT gate the SSH operations
50
+ * capability (host manager, ssh_* tools, /api/dsh-ssh, web terminal — its
51
+ * own `dsh-ssh` settings namespace `enabled`), the shared
52
+ * engine/host-store/vault, the fs/subprocess routing rows, or the seam
53
+ * replacement itself: those mount before this switch and keep running.
54
+ */
36
55
  enabled: boolean;
37
56
  /** Whether the model-facing announcement section is mounted. */
38
57
  announceToAgent: boolean;
39
58
  /**
40
59
  * Secret storage mode: 'none' (default, VSCode Remote-SSH style: passwords
41
60
  * never persisted, prompted once per session) or 'vault' (encrypted at
42
- * rest, for unattended agents). Mirrors the dsh-ssh settings namespace.
61
+ * rest, for unattended agents).
62
+ *
63
+ * This is the SINGLE SOURCE for the mode: the Vault and SecureHostStore are
64
+ * constructed once from it when the plugin loads, so changing it requires a
65
+ * plugin reload. The `dsh-ssh` settings namespace also exposes
66
+ * `secretStorage` for the settings UI, but that value cannot switch storage
67
+ * mode at runtime (see reportSecretStorageDrift below).
43
68
  */
44
69
  secretStorage: 'none' | 'vault';
70
+ /**
71
+ * Whether the credential vault may auto-unlock from the
72
+ * `DSH_CREDENTIAL_PASSWORD` environment variable at plugin load.
73
+ *
74
+ * `'off'` (default): the vault stays locked until a password is entered, so a
75
+ * leaked ciphertext is not enough to recover credentials. `'env'` is the
76
+ * explicit opt-in for unattended/headless agents.
77
+ */
78
+ vaultAutoUnlock: 'off' | 'env';
45
79
  }
46
80
  export declare const Config: z<Config>;
81
+ /**
82
+ * The ONE effective secretStorage mode: the plugin config value, defaulting to
83
+ * 'none'. The Vault and SecureHostStore are constructed from this value once
84
+ * (plugin load), which is what makes it authoritative.
85
+ */
86
+ export declare function resolveSecretStorageMode(config?: {
87
+ secretStorage?: unknown;
88
+ }): 'none' | 'vault';
89
+ /**
90
+ * Report a secretStorage disagreement between the RUNNING mode (fixed when the
91
+ * Vault/SecureHostStore were constructed at plugin load) and the value the
92
+ * `dsh-ssh` settings namespace currently resolves to. The mode cannot change
93
+ * at runtime, so staying silent would let the settings UI claim a storage mode
94
+ * this process is not using.
95
+ * @param running - the construction-time mode actually in effect.
96
+ * @param section - the resolved `dsh-ssh` settings section (any shape).
97
+ * @returns the warning text, or undefined when both agree / the value is unusable.
98
+ */
99
+ export declare function secretStorageDriftMessage(running: 'none' | 'vault', section: unknown): string | undefined;
100
+ /**
101
+ * Report secretStorage drift once at mount and on every committed settings
102
+ * change (deduplicated, so a repeated change does not spam the log). The
103
+ * settings service is a sibling context and cordis events travel UP from the
104
+ * emitting ctx, so the listener sits on the root context; `ctx.effect` keeps
105
+ * the subscription tied to this plugin's lifetime.
106
+ * @param ctx - the plugin context (provides `settings` when present).
107
+ * @param running - the construction-time mode actually in effect.
108
+ */
109
+ export declare function watchSecretStorageDrift(ctx: Context, running: 'none' | 'vault'): void;
110
+ /** Isolated-path options for one generic startup (tests inject temp dirs). */
111
+ export interface GenericBootOptions {
112
+ /** The frozen legacy SSH ledger file (~/.dsh/dsh-hardssh-workspaces.json). */
113
+ legacyPath: string;
114
+ /** The generic ledger target (~/.dsh/workspaces/index.v1.json). */
115
+ genericPath: string;
116
+ /** Migration report path. The report is also the durable cutover marker. */
117
+ reportPath?: string;
118
+ }
119
+ /**
120
+ * Run the Phase-7 cutover sequence against one generic core. A structurally
121
+ * valid atomic migration report is the durable cutover marker: once it exists,
122
+ * the generic ledger is permanently authoritative and startup never compares
123
+ * it with (or even reads) the frozen legacy source again. Without a marker the
124
+ * one-time migration runs and atomically publishes that report only after the
125
+ * generic snapshot has committed and verified.
126
+ *
127
+ * A valid marker is also a proof of existence: it records that N workspaces
128
+ * were committed, so a MISSING or UNREADABLE ledger underneath it is data loss,
129
+ * not a fresh deployment. `WorkspaceLedger` treats ENOENT as an empty array,
130
+ * which would silently drop every workspace forever (the marker suppresses the
131
+ * one-time import); this gate therefore recovers the ledger — `.last-good`,
132
+ * then the newest `.backup-*`, then a legacy re-import — and, failing that,
133
+ * refuses to start so the surfaces fail closed instead of showing nothing.
134
+ *
135
+ * The recovery is verified against the marker's own id list, so a source that
136
+ * would restore FEWER workspaces than were recorded is rejected instead of
137
+ * accepted as success. A ledger that exists but is corrupt is treated like a
138
+ * missing one; a ledger that exists and is valid is authoritative even when it
139
+ * is empty (deleting every workspace is a legitimate, recorded intent).
140
+ *
141
+ * On any failure the promise rejects and the core stays NOT ready, so workspace
142
+ * consumers fail closed instead of silently operating local files.
143
+ */
144
+ export declare function bootstrapGenericWorkspaceCore(core: WorkspaceCore, options: GenericBootOptions): Promise<void>;
145
+ /** Local-session branch of the announcement. Exported for the guidance test. */
146
+ export declare function localGuidance(): string;
147
+ /**
148
+ * SSH-bound-session branch of the announcement.
149
+ *
150
+ * The tool facts here must match the seams. read/write/edit route through
151
+ * `SwitchFileSystem` (server data remote, client-declared local roots local).
152
+ * glob/grep do NOT: they run the CLIENT's bundled ripgrep through
153
+ * `ctx.subprocess`, which cannot see the remote workspace, so the subprocess
154
+ * facade refuses that combination and points here to the remote tools. `pwsh`
155
+ * is a client-native binary and runs on this machine.
156
+ *
157
+ * Exported for the guidance test.
158
+ */
159
+ export declare function remoteGuidance(record: SshWorkspaceRecord): string;
160
+ /**
161
+ * A-04 startup half: replay host-workspace registration for every stored
162
+ * record once the workspace store is ready, so a process restart (or a create
163
+ * whose registration failed before the compensating rollback existed) cannot
164
+ * leave a binding whose sidebar entry is missing. `registerHostWorkspace` is
165
+ * create-if-missing, so replay is idempotent.
166
+ *
167
+ * Thin delegation to the shared `reconcileHostWorkspaces` (src/routes.ts) that
168
+ * the loopback-guarded `/reconcile` route also calls, so the two halves cannot
169
+ * drift. This wrapper owns the startup-only rules: never reject (a broken host
170
+ * registry or an unreadable ledger must not break plugin load) and log what
171
+ * happened instead of surfacing it to the loader.
172
+ * @param deps - the record source and the host-registry registration hook.
173
+ * @returns the shared reconcile report (empty when the record list failed).
174
+ */
175
+ export declare function reconcileHostWorkspacesOnStartup(deps: HostWorkspaceReconcileDeps): Promise<HostWorkspaceReconcileReport>;
47
176
  /**
48
177
  * Mount the mode store, routes, tools, announcement, guard, and the shared core.
49
178
  * @param ctx - host plugin context carrying tools/systemPrompt (webServer optional).
@@ -1,46 +1,19 @@
1
1
  /**
2
- * SSH-bound workspace ledger: the mapping from a LOCAL ANCHOR directory (the
3
- * host-visible workspace path, which sessions use as their cwd) to a REMOTE
4
- * directory on an SSH host. Persisted as JSON next to dsh-ssh.json so the
5
- * fs/subprocess seams can route a session whose cwd is an anchor to the
6
- * remote execution world, and everything else stays local.
2
+ * SSH-bound workspace path helpers.
7
3
  *
8
- * A ledger record is created through the GUI (pick a host, browse a remote
9
- * dir, give it a title). The host-side anchor directory is auto-created
10
- * under ~/.dsh/ssh-workspaces/<id>/; the host workspace registry owns it as
11
- * a normal local workspace, so session creation/listing/persistence keep
12
- * working untouched.
13
- *
14
- * Mutations are serialized through a per-instance queue and committed as:
15
- * write temp file → atomic rename → swap memory/index → bump revision →
16
- * notify subscribers. A persistence failure leaves observable state
17
- * unchanged and never publishes an event.
4
+ * This module is the last remaining piece of the retired SSH ledger module:
5
+ * the `SshWorkspaceLedger` class was removed with the generic cutover, so what
6
+ * survives here are the anchor/path helpers the generic runtime still shares
7
+ * the managed SSH anchor layout (`~/.dsh/ssh-workspaces/<id>`), the legacy
8
+ * ledger file location (the one-time import source), the client DTO title
9
+ * default, and the lexical anchor-containment helpers used by fs/subprocess
10
+ * fail-closed gating. Record persistence itself is owned by the generic
11
+ * `WorkspaceLedger` (`./base/ledger.ts`).
18
12
  *
19
13
  * @module dsh-hardssh/ledger
20
14
  */
21
- import type { SshWorkspaceRecord } from './protocol.ts';
22
- /** One committed snapshot of the ledger. */
23
- export interface LedgerSnapshot {
24
- revision: number;
25
- records: readonly SshWorkspaceRecord[];
26
- }
27
- /** One committed mutation, delivered to subscribers. */
28
- export type LedgerChange = {
29
- type: 'created';
30
- revision: number;
31
- record: SshWorkspaceRecord;
32
- } | {
33
- type: 'renamed';
34
- revision: number;
35
- before: SshWorkspaceRecord;
36
- record: SshWorkspaceRecord;
37
- } | {
38
- type: 'removed';
39
- revision: number;
40
- record: SshWorkspaceRecord;
41
- };
42
- export type LedgerListener = (change: LedgerChange) => void;
43
- /** Ledger file location: ~/.dsh/dsh-hardssh-workspaces.json. */
15
+ /** The frozen legacy SSH ledger location: ~/.dsh/dsh-hardssh-workspaces.json.
16
+ * Only read once by the startup import when no cutover marker exists. */
44
17
  export declare function ledgerPath(): string;
45
18
  /**
46
19
  * Normalize a remote POSIX root: keep '/' as '/', collapse repeated
@@ -55,78 +28,10 @@ export declare function anchorPathFor(id: string): string;
55
28
  /** Default record title when the user gives none. */
56
29
  export declare function defaultTitle(remoteRoot: string, alias: string): string;
57
30
  /**
58
- * Normalize a local anchor path for comparison: Windows anchors are
59
- * case-insensitive with mixed separators; POSIX anchors stay case-sensitive.
60
- */
61
- export declare function normalizeAnchorPath(path: string): string;
62
- /** True when candidate equals anchor or is one of its descendants. Lexical
63
- * comparison over already-resolved paths; deliberately no fs access. */
64
- export declare function isPathUnderAnchor(anchor: string, candidate: string): boolean;
65
- /**
66
- * The in-memory ledger. Loaded lazily on first access; every mutation writes
67
- * through durably (atomic rename) and is serialized through a per-instance
68
- * queue. A synchronous anchor index is maintained from the latest committed
69
- * records so host seams can resolve a cwd/root without awaiting I/O.
31
+ * Anchor-path comparison is owned by ONE implementation (`base/ledger.ts`):
32
+ * two copies had already drifted in their Windows/UNC root handling, and the
33
+ * seams, the store, and the client all compare anchors. Re-exported here so
34
+ * existing `from './ledger.ts'` imports keep working.
70
35
  */
71
- export declare class SshWorkspaceLedger {
72
- private readonly fileOverride?;
73
- private readonly anchorOverride?;
74
- private records;
75
- private loadPromise;
76
- private anchorIndex;
77
- private mutationTail;
78
- private currentRevision;
79
- private readonly listeners;
80
- /** Overridable file location (tests isolate per instance). */
81
- constructor(fileOverride?: string | undefined, anchorOverride?: string | undefined);
82
- private file;
83
- private anchors;
84
- private anchorFor;
85
- /** Load the ledger once (missing/unreadable/malformed file -> empty ledger). */
86
- private ensureLoaded;
87
- private readRecords;
88
- /** Rebuild the synchronous anchor index from the current records. */
89
- private reindex;
90
- /** Persist a proposed record array without touching observable state. */
91
- private save;
92
- /** Serialize one mutation; a rejected earlier mutation never poisons later ones. */
93
- private enqueueMutation;
94
- /** Commit a computed next state: swap memory, bump revision, notify. */
95
- private commit;
96
- private emit;
97
- /** Load the ledger (compat; returns detached copies). */
98
- load(): Promise<SshWorkspaceRecord[]>;
99
- /** All records, in creation order (detached copies). */
100
- list(): Promise<SshWorkspaceRecord[]>;
101
- /** Current detached snapshot. */
102
- snapshot(): Promise<LedgerSnapshot>;
103
- /** Current in-process snapshot, synchronously (empty before first load).
104
- * The returned records are the live immutable objects — callers must not
105
- * mutate them (commits always replace the array wholesale). */
106
- snapshotSync(): LedgerSnapshot;
107
- /** Current in-process revision (restarts at 0 on process restart). */
108
- revision(): number;
109
- /** Subscribe to committed mutations (no initial event). Returns a disposer. */
110
- subscribe(listener: LedgerListener): () => void;
111
- /** Look up by id. */
112
- get(id: string): Promise<SshWorkspaceRecord | undefined>;
113
- /** Look up by LOCAL anchor path (resolved). Returns the record whose
114
- * anchor owns the path (the anchor itself or any descendant). */
115
- findByAnchor(path: string): Promise<SshWorkspaceRecord | undefined>;
116
- /** Synchronous anchor lookup (uses the in-memory index). */
117
- findByAnchorSync(path: string): SshWorkspaceRecord | undefined;
118
- /** Create a record: materialize the anchor, then persist+commit. */
119
- create(input: {
120
- title: string;
121
- alias: string;
122
- remoteRoot: string;
123
- }): Promise<SshWorkspaceRecord>;
124
- /** Rename a record (display title only). */
125
- rename(id: string, title: string): Promise<SshWorkspaceRecord | undefined>;
126
- /** Remove a record. The anchor directory is left in place (a host
127
- * workspace may still reference it; the caller decides on deletion). */
128
- remove(id: string): Promise<boolean>;
129
- /** The anchor root shared by every record (for UI hints). */
130
- anchorsRoot(): string;
131
- }
36
+ export { isPathUnderAnchor, normalizeAnchorPath } from './base/ledger.ts';
132
37
  //# sourceMappingURL=ledger.d.ts.map
@@ -62,9 +62,6 @@ export interface ApiErrorBody {
62
62
  export declare const WORKSPACE_API_BASE: "/api/dsh-hardssh";
63
63
  export declare const WORKSPACE_API: {
64
64
  readonly state: string;
65
- readonly tree: string;
66
- readonly file: string;
67
- readonly search: string;
68
65
  readonly sshWorkspaces: string;
69
66
  readonly sshWorkspaceDir: string;
70
67
  };
@@ -89,8 +86,6 @@ export interface SshWorkspaceRecord {
89
86
  /** When the record was created. */
90
87
  createdAt: string;
91
88
  }
92
- /** The ledger of SSH-bound workspaces (persisted by the host). */
93
- export type SshWorkspaceLedger = SshWorkspaceRecord[];
94
89
  /** One remote directory entry for the picker tree. */
95
90
  export interface RemoteDirEntry {
96
91
  name: string;
@@ -8,11 +8,14 @@
8
8
  import type { WorkspaceProviderRegistry } from '../base/registry.ts';
9
9
  import type { SshEngine } from '../ssh/engine.ts';
10
10
  import type { HostStoreView } from '../core.ts';
11
- /** Register both built-in providers. Returns per-provider disposers. */
12
- export declare function registerBuiltinProviders(registry: WorkspaceProviderRegistry, deps?: {
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ /** Register both built-in providers. Returns per-provider disposers. The
13
+ * Cordis context is mandatory: both providers serve only the real DSH
14
+ * fs/process capabilities, which need a per-workspace Cordis scope. */
15
+ export declare function registerBuiltinProviders(registry: WorkspaceProviderRegistry, deps: {
13
16
  engine?: SshEngine;
14
17
  hosts?: HostStoreView;
15
- }): Array<() => void>;
18
+ }, context: Context): Array<() => void>;
16
19
  /** The canonical provider ids. */
17
20
  export declare const BUILTIN_PROVIDER_IDS: readonly ["local", "ssh"];
18
21
  export type BuiltinProviderId = (typeof BUILTIN_PROVIDER_IDS)[number];
@@ -1,39 +1,82 @@
1
1
  /**
2
- * Local provider the workspace provider for local-disk workspaces. Direct
3
- * Node fs implementation of `WorkspaceFileSystem`, so the local provider is
4
- * usable OUTSIDE the dsh harness too (a consumer only needs `base`).
5
- *
6
- * The DSH runtime later re-wraps this over `dsh-fs-sandbox` / `dsh-fs-local`
7
- * for sandbox semantics when running inside the harness; the base contract
8
- * stays identical.
2
+ * Local workspace provider. Inside DSH it exposes the official FileSystem and
3
+ * SubprocessRuntime capability shapes, both confined to the record root. Pure
4
+ * base consumers without Cordis keep the lightweight path-style fallbacks.
9
5
  *
10
6
  * @module @tiphareth/dsh-hardssh/providers/local
11
7
  */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ import type { FsPathInfo, FsTarget } from '@deepseek-ai/dsh-fs';
10
+ import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local';
11
+ import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
12
+ import { LocalSubprocessRuntime } from '@deepseek-ai/dsh-subprocess-local';
12
13
  import type { WorkspaceCapabilityMap, WorkspaceConnection, WorkspaceProvider, WorkspaceProviderManifest } from '../../base/model.ts';
13
- import type { WorkspaceDirEntry, WorkspaceFileSystem, WorkspaceStat } from '../../base/capability.ts';
14
- /** The local provider manifest. */
14
+ import type { WorkspaceDirEntry, WorkspaceFileSystem, WorkspaceProcessRuntime, WorkspaceStat } from '../../base/capability.ts';
15
+ /** The local provider implements the same official v2 fs/process keys as SSH. */
15
16
  export declare const localProviderManifest: WorkspaceProviderManifest;
16
- /** One open local workspace connection. */
17
+ /** Official DSH local filesystem with the record root promoted to a hard jail. */
18
+ export declare class RootedLocalFileSystem extends LocalFileSystem {
19
+ readonly workspaceRoot: string;
20
+ constructor(ctx: Context, root: string);
21
+ /** LocalFileSystem.resolve() has no root jail, so this override adds one. */
22
+ resolve(path: string, opts?: {
23
+ cwd?: string;
24
+ signal?: AbortSignal;
25
+ }): Promise<FsTarget>;
26
+ /** LocalFileSystem.lstat() bypasses resolve(), so it needs the same explicit jail. */
27
+ lstat(path: string, opts?: {
28
+ cwd?: string;
29
+ }, signal?: AbortSignal): Promise<FsPathInfo | undefined>;
30
+ /** LocalFileSystem maps every host path; a rooted capability must hide outsiders. */
31
+ processPathFromHostPath(hostPath: string): string | undefined;
32
+ }
33
+ /**
34
+ * Per-connection root guard over one shared official local subprocess runtime.
35
+ * LocalSubprocessRuntime cannot bind a record root and cannot tear down only
36
+ * one workspace's handles, so this facade adds both missing lifecycle facts.
37
+ */
38
+ export declare class RootedLocalSubprocessRuntime {
39
+ private readonly runtime;
40
+ readonly workspaceRoot: string;
41
+ private readonly processes;
42
+ private readonly terminals;
43
+ constructor(runtime: LocalSubprocessRuntime, workspaceRoot: string);
44
+ /** The shared runtime already owns executable lookup; no root path is involved. */
45
+ resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
46
+ /** LocalSubprocessRuntime.spawn() accepts any cwd, so this method fences it. */
47
+ spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
48
+ /** LocalSubprocessRuntime.spawnTerminal() accepts any cwd, so this method fences it. */
49
+ spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>;
50
+ /** The shared runtime cannot close one workspace, so terminate only owned handles. */
51
+ close(): Promise<void>;
52
+ }
53
+ /** One open local workspace connection with identity-cached capabilities. */
17
54
  export declare class LocalWorkspaceConnection implements WorkspaceConnection {
18
55
  readonly workspaceId: string;
19
56
  private readonly root;
57
+ private readonly cordisContext?;
58
+ private readonly officialProcess?;
20
59
  readonly providerId = "local";
21
- constructor(workspaceId: string, root: string);
60
+ private state;
61
+ private fsInstance;
62
+ private processInstance;
63
+ constructor(workspaceId: string, root: string, cordisContext?: Context | undefined, officialProcess?: LocalSubprocessRuntime | undefined);
64
+ /** A static manifest cannot return instances, so get() lazily caches each capability. */
22
65
  get<K extends keyof WorkspaceCapabilityMap>(capability: K): WorkspaceCapabilityMap[K] | undefined;
66
+ /** The old path-style wrapper is not a DSH FileSystem, so Cordis uses the official rooted backend. */
67
+ private fs;
68
+ /** The fs capability cannot spawn processes, so process has its own official rooted facade. */
69
+ private process;
70
+ /** Same honesty rule as the SSH connection: only states this object owns. */
23
71
  status(): 'connecting' | 'ready' | 'degraded' | 'closed';
72
+ /** Dropping references alone cannot stop live local processes, so close the rooted facade first. */
24
73
  close(): Promise<void>;
25
74
  }
26
- /**
27
- * Node-fs implementation of `WorkspaceFileSystem` confined to a root.
28
- * Workspace-relative paths (`/src/index.ts`, `src/index.ts`) are resolved
29
- * against the root; `..` escapes are rejected; symlinks are not followed
30
- * out of the root by the stat path (defense in depth — the contract says
31
- * the provider enforces confinement).
32
- */
75
+ /** Standalone path-style filesystem fallback for consumers without Cordis. */
33
76
  export declare class LocalWorkspaceFileSystem implements WorkspaceFileSystem {
34
77
  private readonly root;
35
78
  constructor(root: string);
36
- /** Resolve a workspace-relative path into an absolute path inside root. */
79
+ /** Node fs accepts arbitrary paths, so every fallback operation resolves through this jail. */
37
80
  private abs;
38
81
  stat(path: string, signal?: AbortSignal): Promise<WorkspaceStat | undefined>;
39
82
  list(path: string, signal?: AbortSignal): Promise<WorkspaceDirEntry[]>;
@@ -48,11 +91,28 @@ export declare class LocalWorkspaceFileSystem implements WorkspaceFileSystem {
48
91
  signal?: AbortSignal;
49
92
  }): Promise<void>;
50
93
  rename(from: string, to: string, signal?: AbortSignal): Promise<void>;
51
- /** Convenience: check the root exists and is readable (for open()). */
94
+ /** WorkspaceProvider.open() cannot infer root availability from stat(undefined), so expose this probe. */
52
95
  accessible(): Promise<boolean>;
53
96
  }
54
- /** The local provider factory. */
55
- export declare function createLocalWorkspaceProvider(): WorkspaceProvider;
56
- /** Helper: title mostly used by record factories. */
97
+ /** Standalone process fallback for base-only consumers without a Cordis runtime. */
98
+ export declare class LocalWorkspaceProcess implements WorkspaceProcessRuntime {
99
+ private readonly root;
100
+ constructor(root: string);
101
+ /** LocalSubprocessRuntime needs Cordis, so standalone exec uses node:child_process with the same root jail. */
102
+ exec(command: string, options?: {
103
+ timeoutMs?: number;
104
+ cwd?: string;
105
+ signal?: AbortSignal;
106
+ }): Promise<{
107
+ stdout: string;
108
+ stderr: string;
109
+ exitCode: number;
110
+ timedOut?: boolean;
111
+ durationMs?: number;
112
+ }>;
113
+ }
114
+ /** The local provider factory; Context is optional only for standalone base consumers/tests. */
115
+ export declare function createLocalWorkspaceProvider(context?: Context): WorkspaceProvider;
116
+ /** basename() alone returns empty for filesystem roots, so retain the root as fallback. */
57
117
  export declare function localDefaultTitle(root: string): string;
58
118
  //# sourceMappingURL=provider.d.ts.map