@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.
- package/LICENSE +30 -0
- package/README.md +49 -25
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1171 -1321
- package/lib/fs.js +307 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +18 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +58 -25
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -96
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +137 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -225
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
package/lib/types/fs.d.ts
CHANGED
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `ctx.fs` switch row: provides the
|
|
3
|
-
* the host scope. The local backend (the deployment's
|
|
4
|
-
* mounted in an isolated child scope so its own
|
|
5
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
24
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
/** Mount the generic switching filesystem facade. */
|
|
35
28
|
export declare function apply(ctx: Context): void;
|
|
36
29
|
//# sourceMappingURL=fs.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
/**
|
|
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).
|
|
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).
|
package/lib/types/ledger.d.ts
CHANGED
|
@@ -1,46 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSH-bound workspace
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
94
|
+
/** WorkspaceProvider.open() cannot infer root availability from stat(undefined), so expose this probe. */
|
|
52
95
|
accessible(): Promise<boolean>;
|
|
53
96
|
}
|
|
54
|
-
/**
|
|
55
|
-
export declare
|
|
56
|
-
|
|
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
|