dsh-plugin-wsl-env 0.6.0 → 0.7.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/CHANGELOG.md +176 -0
- package/README.md +6 -6
- package/cordis.patch.yml +48 -0
- package/docs/ARCHITECTURE.md +33 -20
- package/docs/CONFIGURATION.md +2 -2
- package/docs/LIMITATIONS.md +52 -54
- package/docs/PARITY.md +18 -4
- package/docs/README.zh.md +7 -7
- package/lib/fs-decisions.js +22 -25
- package/lib/fs-routing.js +176 -0
- package/lib/git-route.js +142 -0
- package/lib/index.js +77 -307
- package/lib/listing.js +47 -0
- package/lib/picker.js +121 -17
- package/lib/search-exec.js +114 -0
- package/lib/search-route.js +95 -0
- package/lib/subprocess.js +153 -16
- package/lib/terminal-activity.js +121 -0
- package/lib/workspace-files-route.js +205 -0
- package/lib/workspace-files-wsl.js +341 -0
- package/lib/wsl.js +0 -39
- package/package.json +22 -3
- package/lib/fs-publish.js +0 -42
package/lib/index.js
CHANGED
|
@@ -36,20 +36,18 @@
|
|
|
36
36
|
* @module dsh-plugin-wsl
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import { rename } from "node:fs/promises";
|
|
40
39
|
import { fileURLToPath } from "node:url";
|
|
41
40
|
import z from "@deepseek-ai/schemastery";
|
|
42
41
|
import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
|
|
43
42
|
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
44
43
|
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
45
44
|
import { canonicalPath, classifyRunnerFailure, isRunnerSpawnFailure, matchesSignature, SandboxUnavailableError, writableRoots } from "@deepseek-ai/dsh-sandbox";
|
|
46
|
-
import { isAnotherDistrosUnc, isRelativeWorldPath, isWorldPathUnder, isWslUnc, isUnderDistro, posixToUnc,
|
|
47
|
-
import {
|
|
45
|
+
import { isAnotherDistrosUnc, isRelativeWorldPath, isWorldPathUnder, isWslUnc, isUnderDistro, posixToUnc, toLinuxPath, toWorldPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
46
|
+
import { isConfinedMutation, mutationModeRefusal, outsideDistroRefusal, shareSubstrateRefusal, substrateFailure, workspaceWriteDenial } from "./fs-decisions.js";
|
|
48
47
|
import { WslSandbox } from "./sandbox.js";
|
|
49
48
|
import { armDistroWatcher } from "./watcher.js";
|
|
50
49
|
import { WslAgent } from "./agent.js";
|
|
51
50
|
import { AgentUnavailableError } from "./agent-errors.js";
|
|
52
|
-
import { publicationArgv } from "./fs-publish.js";
|
|
53
51
|
import { agentExecutionHandle } from "./agent-exec.js";
|
|
54
52
|
import { sharedAgent } from "./agent-shared.js";
|
|
55
53
|
import { AgentSubstrate } from "./fs-substrate.js";
|
|
@@ -57,7 +55,7 @@ import { FsCodedError } from "./fsio-text.js";
|
|
|
57
55
|
import { confinedAgent } from "./agent-confined.js";
|
|
58
56
|
// `runCapture` and distro discovery live in ./wsl.js so the directory picker
|
|
59
57
|
// can use them without pulling this file's executor and filesystem peers in.
|
|
60
|
-
import {
|
|
58
|
+
import { defaultDistro, defaultShell, linuxHomePath, shellArgs, workdirFailure, wslErrorCode } from "./wsl.js";
|
|
61
59
|
|
|
62
60
|
/**
|
|
63
61
|
* Every model shell call gets a rebuilt managed `DSH_*` namespace (see
|
|
@@ -779,15 +777,6 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
779
777
|
* workspace the operator selected.
|
|
780
778
|
*/
|
|
781
779
|
restrictToDistro: z.boolean().default(true),
|
|
782
|
-
/**
|
|
783
|
-
* When a read-side operation reports `FS_NOT_FOUND` on a Linux path, ask the
|
|
784
|
-
* distro for that path's canonical form and retry once. This is what makes
|
|
785
|
-
* symlinked paths usable at all: the UNC share cannot traverse Linux
|
|
786
|
-
* symlinks, so `/etc/os-release`, `/bin` and `/lib` would otherwise be
|
|
787
|
-
* unreachable from the model's read tools. Costs one `wsl.exe` call per
|
|
788
|
-
* distinct failing path, cached.
|
|
789
|
-
*/
|
|
790
|
-
resolveSymlinks: z.boolean().default(true),
|
|
791
780
|
/**
|
|
792
781
|
* How deep the in-distro watch scan walks, `1` = the watched directory and
|
|
793
782
|
* its direct children. `0` — the default — walks the whole tree, which is
|
|
@@ -798,68 +787,39 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
798
787
|
*/
|
|
799
788
|
watchMaxDepth: z.natural().default(0),
|
|
800
789
|
/**
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* `wsl.exe` chmod plus a 9p rename. Any agent failure falls back to the
|
|
804
|
-
* legacy two-step path for that call and, after the agent's own rebuild
|
|
805
|
-
* budget is spent, permanently — this flag never changes what is published,
|
|
806
|
-
* only how many round trips it costs.
|
|
807
|
-
*/
|
|
808
|
-
agent: z.boolean().default(true),
|
|
809
|
-
/**
|
|
810
|
-
* Which I/O substrate serves the file tools. `"agent"` (the default) —
|
|
811
|
-
* the resident in-distro agent (`lib/fs-substrate.js`): reads, writes and
|
|
790
|
+
* Which I/O substrate serves the file tools. Only `"agent"` exists — the
|
|
791
|
+
* resident in-distro agent (`lib/fs-substrate.js`): reads, writes and
|
|
812
792
|
* identities run where the files live, symlinks and mode bits are native,
|
|
813
793
|
* and the mutation guard survives to the write's no-replace publication,
|
|
814
794
|
* kernel-enforced by the confined resident when the policy is confined.
|
|
815
|
-
* `"share"`
|
|
816
|
-
* the distro's 9p share
|
|
817
|
-
* that
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
795
|
+
* The former `"share"` opt-out — the Windows-side host filesystem stack
|
|
796
|
+
* over the distro's 9p share — is retired: it was the one model-facing
|
|
797
|
+
* I/O path that still crossed the share, and the agent has no fallback
|
|
798
|
+
* that needs it. A profile carrying `"share"` is refused at construction
|
|
799
|
+
* with the migration (`shareSubstrateRefusal`). `watch()` rides the
|
|
800
|
+
* in-distro poll loop for the substrate regardless (`lib/watcher.js`) —
|
|
801
|
+
* it never ran through either substrate. The substrate never changes
|
|
802
|
+
* `sandboxMode`'s honesty.
|
|
822
803
|
*/
|
|
823
804
|
substrate: z.union(["share", "agent"]).default("agent"),
|
|
824
805
|
});
|
|
825
806
|
|
|
826
807
|
constructor(ctx, config) {
|
|
808
|
+
// The share substrate is refused before any peer work: a profile still
|
|
809
|
+
// carrying the retired value must fail loudly, with the migration, not
|
|
810
|
+
// silently serve the one I/O path this provider exists to close.
|
|
811
|
+
const refusal = shareSubstrateRefusal(config.substrate);
|
|
812
|
+
if (refusal !== null) throw new Error(refusal.message);
|
|
827
813
|
super(ctx, config);
|
|
828
814
|
this.resolvedDistro = config.distro;
|
|
829
815
|
/** Cached distro-user home, the base for relative input when `cwd` is empty. */
|
|
830
816
|
this.resolvedHome = undefined;
|
|
831
817
|
/** The deployment default mode — the capability fact the tool layer reads. */
|
|
832
818
|
this.defaultMode = ctx.sandboxPolicy.defaultMode;
|
|
833
|
-
/** targetKey -> canonical replacement target, for symlinked paths. */
|
|
834
|
-
this.canonicalTargets = new Map();
|
|
835
819
|
/** The lazy agent-backed substrate, built on first use (see `agentSubstrate`). */
|
|
836
820
|
this.agentSubstrateInstance = undefined;
|
|
837
821
|
/** The distro-side bwrap usability probe, shared with the confined agents. */
|
|
838
822
|
this.sandbox = new WslSandbox({ wslPath: config.wslPath, maskWindowsDrive: config.maskWindowsDrive });
|
|
839
|
-
// `dsh-fs-local` takes a Windows branch whenever the destination already
|
|
840
|
-
// exists — which is how it knows to preserve the replaced file's security
|
|
841
|
-
// descriptor, and what makes every write to an existing file fail here:
|
|
842
|
-
//
|
|
843
|
-
// * it reads that descriptor with `GetFileSecurityW` and applies it to the
|
|
844
|
-
// staged temp. A 9p share carries no Windows security descriptor, so the
|
|
845
|
-
// call fails with `EIO (Win32 1)` and takes the whole write with it —
|
|
846
|
-
// `edit` and every overwrite, not just an exotic path.
|
|
847
|
-
// * it publishes with `ReplaceFileW`, which is not a 9p primitive.
|
|
848
|
-
//
|
|
849
|
-
// Both are replaced with their POSIX equivalents: the staged temp gets the
|
|
850
|
-
// target's real mode from inside the distro (see `copyModeInDistro` — the
|
|
851
|
-
// share ignores a host-side chmod), and publication is the plain atomic
|
|
852
|
-
// rename the share does support.
|
|
853
|
-
//
|
|
854
|
-
// Trade-off worth knowing: those two hooks are the only DACL handling in the
|
|
855
|
-
// inherited backend, so a *Windows* path served by this provider loses its
|
|
856
|
-
// descriptor preservation. This provider exists to serve distro paths, where
|
|
857
|
-
// no DACL can exist in the first place.
|
|
858
|
-
this.internals = {
|
|
859
|
-
...this.internals,
|
|
860
|
-
copyFileDacl: async () => {},
|
|
861
|
-
replaceFile: (replaced, replacement) => this.publish(replaced, replacement),
|
|
862
|
-
};
|
|
863
823
|
}
|
|
864
824
|
|
|
865
825
|
/**
|
|
@@ -873,50 +833,6 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
873
833
|
return this.config.sandbox ? this.defaultMode : undefined;
|
|
874
834
|
}
|
|
875
835
|
|
|
876
|
-
/**
|
|
877
|
-
* Publish a staged temp file over its target the way a 9p share supports:
|
|
878
|
-
* give the temp the target's POSIX mode inside the distro, then rename it into
|
|
879
|
-
* place.
|
|
880
|
-
*
|
|
881
|
-
* The mode is applied before the rename on purpose. The share drops a mode set
|
|
882
|
-
* from the Windows side, but a rename preserves the inode's mode — so setting
|
|
883
|
-
* it on the staged temp is what makes the published file keep the bits it had,
|
|
884
|
-
* without a second in-distro call after publication and without the version
|
|
885
|
-
* the write returns going stale behind a late `chmod`.
|
|
886
|
-
*
|
|
887
|
-
* @param replaced - the existing file being replaced (a UNC path).
|
|
888
|
-
* @param replacement - the staged temp file holding the new content.
|
|
889
|
-
*/
|
|
890
|
-
async publish(replaced, replacement) {
|
|
891
|
-
const source = uncToPosix(replaced);
|
|
892
|
-
const staged = uncToPosix(replacement);
|
|
893
|
-
if (source !== undefined && staged !== undefined) {
|
|
894
|
-
if (this.config.agent) {
|
|
895
|
-
try {
|
|
896
|
-
// One in-distro round trip: mode copy + atomic rename. The staged
|
|
897
|
-
// temp is on the share, but the agent reaches it by its Linux path,
|
|
898
|
-
// where the kernel does the rename — no 9p publication involved.
|
|
899
|
-
const agent = await this.publicationAgent();
|
|
900
|
-
const result = await agent.exec({
|
|
901
|
-
cwd: "/",
|
|
902
|
-
argv: publicationArgv(source.linuxPath, staged.linuxPath),
|
|
903
|
-
timeoutMs: 30000,
|
|
904
|
-
});
|
|
905
|
-
if (result.exitCode === 0) return;
|
|
906
|
-
// A nonzero from the script is a real publication failure (chmod or
|
|
907
|
-
// rename); report it rather than retrying through the legacy path,
|
|
908
|
-
// which would fail the same way but slower.
|
|
909
|
-
throw new FsError(`publishing "${replaced}" failed inside the distro (exit ${result.exitCode})`, "FS_IO_ERROR");
|
|
910
|
-
} catch (error) {
|
|
911
|
-
// The agent being out is the ONLY thing the legacy path exists for.
|
|
912
|
-
if (!(error instanceof AgentUnavailableError)) throw error;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
await copyModeInDistro(source.distro, source.linuxPath, staged.linuxPath, { wslPath: this.config.wslPath });
|
|
916
|
-
}
|
|
917
|
-
await rename(replacement, replaced);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
836
|
/**
|
|
921
837
|
* The resident agent serving this filesystem's in-distro side work, created
|
|
922
838
|
* on first use and kept for the provider's lifetime (its own idle timer
|
|
@@ -931,13 +847,11 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
931
847
|
}
|
|
932
848
|
|
|
933
849
|
/**
|
|
934
|
-
* The agent-backed substrate
|
|
935
|
-
*
|
|
936
|
-
* @returns {Promise<AgentSubstrate
|
|
937
|
-
* when the share backend is configured.
|
|
850
|
+
* The agent-backed substrate, built on first use over the same resident
|
|
851
|
+
* agent every other distro side work shares.
|
|
852
|
+
* @returns {Promise<AgentSubstrate>} the substrate.
|
|
938
853
|
*/
|
|
939
854
|
async agentSubstrate() {
|
|
940
|
-
if (this.config.substrate !== "agent") return undefined;
|
|
941
855
|
if (this.agentSubstrateInstance === undefined) {
|
|
942
856
|
this.agentSubstrateInstance = new AgentSubstrate({
|
|
943
857
|
agent: await this.publicationAgent(),
|
|
@@ -1046,19 +960,13 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1046
960
|
*/
|
|
1047
961
|
async resolve(path, opts) {
|
|
1048
962
|
const world = await this.worldPath(path, opts?.cwd);
|
|
1049
|
-
const
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
throw this.peerError(error);
|
|
1057
|
-
}
|
|
963
|
+
const linux = uncToPosix(world)?.linuxPath ?? windowsToLinuxMount(world);
|
|
964
|
+
if (linux === undefined) throw new FsError(`cannot serve "${path}" from inside the distro`, "FS_IO_ERROR");
|
|
965
|
+
try {
|
|
966
|
+
return await (await this.agentSubstrate()).resolve(linux, opts?.signal);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
throw this.peerError(error);
|
|
1058
969
|
}
|
|
1059
|
-
const distro = await this.distro();
|
|
1060
|
-
const target = await super.resolve(world, opts);
|
|
1061
|
-
return { targetKey: target.targetKey, displayPath: toDisplayPath(target.targetKey, distro) };
|
|
1062
970
|
}
|
|
1063
971
|
|
|
1064
972
|
/**
|
|
@@ -1095,185 +1003,78 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1095
1003
|
throw new FsError(denial.message, denial.code);
|
|
1096
1004
|
}
|
|
1097
1005
|
|
|
1098
|
-
|
|
1099
|
-
* The canonical replacement target for a Linux symlink this share cannot
|
|
1100
|
-
* traverse, or undefined when the path is not one.
|
|
1101
|
-
* @param target - the resolved target that failed a read-side operation.
|
|
1102
|
-
* @param signal - optional cancellation.
|
|
1103
|
-
* @returns the replacement target, or undefined.
|
|
1104
|
-
*/
|
|
1105
|
-
async canonicalTarget(target, signal) {
|
|
1106
|
-
if (!this.config.resolveSymlinks) return undefined;
|
|
1107
|
-
const cached = this.canonicalTargets.get(target.targetKey);
|
|
1108
|
-
if (cached !== undefined) return cached;
|
|
1109
|
-
const parsed = uncToPosix(target.targetKey);
|
|
1110
|
-
if (parsed === undefined) return undefined;
|
|
1111
|
-
// The agent serves the canonical path when it is up (no extra `wsl.exe`
|
|
1112
|
-
// per distinct failing path); the one-shot helper is the fallback and the
|
|
1113
|
-
// `agent: false` path.
|
|
1114
|
-
let canonical;
|
|
1115
|
-
if (this.config.agent) {
|
|
1116
|
-
try {
|
|
1117
|
-
const agent = await this.publicationAgent();
|
|
1118
|
-
const result = await agent.exec({ cwd: "/", argv: ["readlink", "-f", parsed.linuxPath], timeoutMs: 10000, signal });
|
|
1119
|
-
const resolved = result.stdout.toString("utf8").trim();
|
|
1120
|
-
canonical = resolved.startsWith("/") ? resolved : undefined;
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
if (!(error instanceof AgentUnavailableError)) throw error;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (canonical === undefined) {
|
|
1126
|
-
canonical = await canonicalLinuxPath(parsed.distro, parsed.linuxPath, { wslPath: this.config.wslPath, signal });
|
|
1127
|
-
}
|
|
1128
|
-
if (canonical === undefined || canonical === parsed.linuxPath) return undefined;
|
|
1129
|
-
const replacement = { targetKey: posixToUnc(parsed.distro, canonical), displayPath: target.displayPath };
|
|
1130
|
-
this.canonicalTargets.set(target.targetKey, replacement);
|
|
1131
|
-
return replacement;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
/**
|
|
1135
|
-
* Run one read-side operation, retrying once through the distro's canonical
|
|
1136
|
-
* path when the share cannot follow a symlink. `displayPath` is preserved, so
|
|
1137
|
-
* the model still sees the path it asked for while `targetKey` names the file
|
|
1138
|
-
* the host can actually open.
|
|
1139
|
-
*
|
|
1140
|
-
* @param target - the resolved target.
|
|
1141
|
-
* @param signal - optional cancellation.
|
|
1142
|
-
* @param operation - the inherited operation to attempt.
|
|
1143
|
-
* @returns the operation's result.
|
|
1144
|
-
*/
|
|
1145
|
-
async withCanonicalRetry(target, signal, operation) {
|
|
1146
|
-
let value;
|
|
1006
|
+
async stat(target, signal) {
|
|
1147
1007
|
try {
|
|
1148
|
-
|
|
1008
|
+
return await (await this.agentSubstrate()).stat(target, signal);
|
|
1149
1009
|
} catch (error) {
|
|
1150
|
-
|
|
1151
|
-
const canonical = await this.canonicalTarget(target, signal);
|
|
1152
|
-
if (canonical === undefined) throw error;
|
|
1153
|
-
return operation(canonical);
|
|
1154
|
-
}
|
|
1155
|
-
// `stat` reports an absent file by RETURNING undefined rather than throwing,
|
|
1156
|
-
// and an untraversable symlink takes exactly that path over this share. The
|
|
1157
|
-
// model-facing read tool branches on precisely that undefined — it resolves,
|
|
1158
|
-
// stats, and renders "not found" itself — so retrying only the throw is not
|
|
1159
|
-
// enough: the silent miss has to be retried here too.
|
|
1160
|
-
if (value === undefined) {
|
|
1161
|
-
const canonical = await this.canonicalTarget(target, signal);
|
|
1162
|
-
if (canonical !== undefined) return operation(canonical);
|
|
1163
|
-
}
|
|
1164
|
-
return value;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
async stat(target, signal) {
|
|
1168
|
-
const sub = await this.agentSubstrate();
|
|
1169
|
-
if (sub !== undefined) {
|
|
1170
|
-
try {
|
|
1171
|
-
return await sub.stat(target, signal);
|
|
1172
|
-
} catch (error) {
|
|
1173
|
-
throw this.peerError(error);
|
|
1174
|
-
}
|
|
1010
|
+
throw this.peerError(error);
|
|
1175
1011
|
}
|
|
1176
|
-
return this.withCanonicalRetry(target, signal, (t) => super.stat(t, signal));
|
|
1177
1012
|
}
|
|
1178
1013
|
|
|
1179
1014
|
/**
|
|
1180
1015
|
* No-follow stat one path. The agent substrate answers from the distro,
|
|
1181
|
-
* where a symlink is a first-class entry
|
|
1182
|
-
* inherited host behaviour.
|
|
1016
|
+
* where a symlink is a first-class entry.
|
|
1183
1017
|
* @param path - a Linux, UNC or Windows path.
|
|
1184
1018
|
* @param opts - optional `cwd` override.
|
|
1185
1019
|
* @param signal - optional cancellation.
|
|
1186
1020
|
* @returns the path info, or undefined when absent.
|
|
1187
1021
|
*/
|
|
1188
1022
|
async lstat(path, opts, signal) {
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
throw this.peerError(error);
|
|
1198
|
-
}
|
|
1023
|
+
const world = await this.worldPath(path, opts?.cwd);
|
|
1024
|
+
const linux = uncToPosix(world)?.linuxPath ?? windowsToLinuxMount(world);
|
|
1025
|
+
if (linux === undefined) throw new FsError(`cannot inspect "${path}" from inside the distro`, "FS_IO_ERROR");
|
|
1026
|
+
try {
|
|
1027
|
+
return await (await this.agentSubstrate()).lstat(linux, signal);
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
throw this.peerError(error);
|
|
1199
1030
|
}
|
|
1200
|
-
return super.lstat(path, opts, signal);
|
|
1201
1031
|
}
|
|
1202
1032
|
|
|
1203
1033
|
async readText(target, signal) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
throw this.peerError(error);
|
|
1210
|
-
}
|
|
1034
|
+
try {
|
|
1035
|
+
return await (await this.agentSubstrate()).readText(target, signal);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
throw this.peerError(error);
|
|
1211
1038
|
}
|
|
1212
|
-
return this.withCanonicalRetry(target, signal, (t) => super.readText(t, signal));
|
|
1213
1039
|
}
|
|
1214
1040
|
|
|
1215
1041
|
async streamText(target, signal) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
} catch (error) {
|
|
1221
|
-
throw this.peerError(error);
|
|
1222
|
-
}
|
|
1042
|
+
try {
|
|
1043
|
+
return await (await this.agentSubstrate()).streamText(target, signal);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
throw this.peerError(error);
|
|
1223
1046
|
}
|
|
1224
|
-
return this.withCanonicalRetry(target, signal, (t) => super.streamText(t, signal));
|
|
1225
1047
|
}
|
|
1226
1048
|
|
|
1227
1049
|
async readBytes(target, signal, maxBytes) {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
throw this.peerError(error);
|
|
1234
|
-
}
|
|
1050
|
+
try {
|
|
1051
|
+
return await (await this.agentSubstrate()).readBytes(target, signal, maxBytes);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
throw this.peerError(error);
|
|
1235
1054
|
}
|
|
1236
|
-
return this.withCanonicalRetry(target, signal, (t) => super.readBytes(t, signal, maxBytes));
|
|
1237
1055
|
}
|
|
1238
1056
|
|
|
1239
1057
|
async readByteRange(target, range, signal) {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
throw this.peerError(error);
|
|
1246
|
-
}
|
|
1058
|
+
try {
|
|
1059
|
+
return await (await this.agentSubstrate()).readByteRange(target, range, signal);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
throw this.peerError(error);
|
|
1247
1062
|
}
|
|
1248
|
-
return this.withCanonicalRetry(target, signal, (t) => super.readByteRange(t, range, signal));
|
|
1249
1063
|
}
|
|
1250
1064
|
|
|
1251
1065
|
/**
|
|
1252
|
-
* List one level
|
|
1253
|
-
*
|
|
1254
|
-
* which would leak `\\wsl.localhost\...` into model-facing output.
|
|
1255
|
-
*
|
|
1066
|
+
* List one directory level. The substrate's rows already carry Linux display
|
|
1067
|
+
* paths and canonical identities — nothing to rewrite.
|
|
1256
1068
|
* @param target - the resolved directory.
|
|
1257
1069
|
* @param signal - optional cancellation.
|
|
1258
1070
|
* @returns the directory rows.
|
|
1259
1071
|
*/
|
|
1260
1072
|
async listDir(target, signal) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
try {
|
|
1266
|
-
return await sub.listDir(target, signal);
|
|
1267
|
-
} catch (error) {
|
|
1268
|
-
throw this.peerError(error);
|
|
1269
|
-
}
|
|
1073
|
+
try {
|
|
1074
|
+
return await (await this.agentSubstrate()).listDir(target, signal);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
throw this.peerError(error);
|
|
1270
1077
|
}
|
|
1271
|
-
const entries = await this.withCanonicalRetry(target, signal, (t) => super.listDir(t, signal));
|
|
1272
|
-
const distro = await this.distro();
|
|
1273
|
-
return entries.map((entry) => ({
|
|
1274
|
-
...entry,
|
|
1275
|
-
target: { targetKey: entry.target.targetKey, displayPath: toDisplayPath(entry.target.targetKey, distro) },
|
|
1276
|
-
}));
|
|
1277
1078
|
}
|
|
1278
1079
|
|
|
1279
1080
|
/**
|
|
@@ -1322,22 +1123,12 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1322
1123
|
}
|
|
1323
1124
|
|
|
1324
1125
|
/**
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
|
|
1331
|
-
async assertGuard(target, expected, signal) {
|
|
1332
|
-
if (expected === undefined) return;
|
|
1333
|
-
const existing = await this.stat(target, signal);
|
|
1334
|
-
const refusal = guardRefusal(expected, existing, target.displayPath);
|
|
1335
|
-
if (refusal !== null) throw new FsError(refusal.message, refusal.code);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
/**
|
|
1339
|
-
* Fence the write by the per-call policy, then keep the guard's observable
|
|
1340
|
-
* behaviour.
|
|
1126
|
+
* Fence the write by the per-call policy, then publish through the
|
|
1127
|
+
* substrate. The fence's fresh target is the one checked and written, so a
|
|
1128
|
+
* refusal costs no I/O and cannot be raced by a swapped symlink; the guard
|
|
1129
|
+
* rides the write op, where the agent re-verifies it one syscall before the
|
|
1130
|
+
* rename and the confined resident enforces it in the kernel.
|
|
1131
|
+
*
|
|
1341
1132
|
* @param target - the resolved target.
|
|
1342
1133
|
* @param content - the full new file content.
|
|
1343
1134
|
* @param expected - the write intent.
|
|
@@ -1347,35 +1138,19 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1347
1138
|
* @returns the write outcome.
|
|
1348
1139
|
*/
|
|
1349
1140
|
async writeText(target, content, expected, signal, sandboxPolicy) {
|
|
1350
|
-
// The policy resolves once: the fence and the confined routing must see
|
|
1351
|
-
// the same grant, or the kernel would enforce something other than what
|
|
1352
|
-
// was checked.
|
|
1353
1141
|
const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
1354
|
-
// The fence runs first and its fresh target is the one checked and written,
|
|
1355
|
-
// so a refusal costs no I/O and cannot be raced by a swapped symlink.
|
|
1356
1142
|
const fenced = await this.checkedTarget(target, policy);
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
// backend documents closes here, exactly as on a Linux host. The policy
|
|
1362
|
-
// also selects the confined resident whose mount table matches it.
|
|
1363
|
-
try {
|
|
1364
|
-
return await sub.writeText(fenced, content, expected, signal, policy);
|
|
1365
|
-
} catch (error) {
|
|
1366
|
-
throw this.peerError(error);
|
|
1367
|
-
}
|
|
1143
|
+
try {
|
|
1144
|
+
return await (await this.agentSubstrate()).writeText(fenced, content, expected, signal, policy);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
throw this.peerError(error);
|
|
1368
1147
|
}
|
|
1369
|
-
await this.assertGuard(fenced, expected, signal);
|
|
1370
|
-
// The guard is deliberately not forwarded: the host backend would publish a
|
|
1371
|
-
// guarded creation with a hard link, which this filesystem rejects. Once
|
|
1372
|
-
// past the guard, publication is the overridden `publish` (see the
|
|
1373
|
-
// constructor), not the inherited Windows one.
|
|
1374
|
-
return super.writeText(fenced, content, undefined, signal);
|
|
1375
1148
|
}
|
|
1376
1149
|
|
|
1377
1150
|
/**
|
|
1378
|
-
* Fence the edit by the same policy, keeping the guard's observable
|
|
1151
|
+
* Fence the edit by the same policy, keeping the guard's observable
|
|
1152
|
+
* behaviour.
|
|
1153
|
+
*
|
|
1379
1154
|
* @param target - the resolved target.
|
|
1380
1155
|
* @param edit - the literal search/replace request.
|
|
1381
1156
|
* @param expected - the version guard.
|
|
@@ -1387,15 +1162,10 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1387
1162
|
async editText(target, edit, expected, signal, sandboxPolicy) {
|
|
1388
1163
|
const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
1389
1164
|
const fenced = await this.checkedTarget(target, policy);
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
} catch (error) {
|
|
1395
|
-
throw this.peerError(error);
|
|
1396
|
-
}
|
|
1165
|
+
try {
|
|
1166
|
+
return await (await this.agentSubstrate()).editText(fenced, edit, expected, signal, policy);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
throw this.peerError(error);
|
|
1397
1169
|
}
|
|
1398
|
-
await this.assertGuard(fenced, expected, signal);
|
|
1399
|
-
return super.editText(fenced, edit, undefined, signal);
|
|
1400
1170
|
}
|
|
1401
1171
|
}
|
package/lib/listing.js
CHANGED
|
@@ -110,3 +110,50 @@ export function boundedInsert(window, candidate, keep) {
|
|
|
110
110
|
window.pop();
|
|
111
111
|
return true;
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The distro-side argv one picker level is listed with.
|
|
116
|
+
*
|
|
117
|
+
* `-1` is one entry per line, `-A` keeps dot-entries but drops `.` and `..`,
|
|
118
|
+
* `-L` follows symlinks so a link to a directory reports as a directory and a
|
|
119
|
+
* broken link drops out (exactly what the per-entry `stat` the host path used
|
|
120
|
+
* to pay one 9p round trip for), and `-p` marks directories with the trailing
|
|
121
|
+
* `/` the parser keys on.
|
|
122
|
+
*
|
|
123
|
+
* @param linuxDir - the absolute Linux directory to list.
|
|
124
|
+
* @returns the argv for the agent or a `wsl.exe --exec` launch.
|
|
125
|
+
*/
|
|
126
|
+
export function lsListingArgv(linuxDir) {
|
|
127
|
+
return ["ls", "-1ALp", "--", linuxDir];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse one `ls -1ALp` listing into the picker's rows.
|
|
132
|
+
*
|
|
133
|
+
* A line is a row when it ends with `/` — a directory, or a symlink `-L`
|
|
134
|
+
* resolved to one; files and dangling links are skipped, matching the host
|
|
135
|
+
* path's behaviour. The parse reads the listing as UTF-8 lines, so a filename
|
|
136
|
+
* containing a newline would split into two rows — the same limitation class
|
|
137
|
+
* the one-shot transport has always had, accepted here with the 9p walk it
|
|
138
|
+
* replaces.
|
|
139
|
+
*
|
|
140
|
+
* @param stdout - the listing's raw stdout.
|
|
141
|
+
* @param parent - the listed directory, in the UNC form the rows carry.
|
|
142
|
+
* @param maxEntries - the row bound; a level with more is flagged truncated.
|
|
143
|
+
* @returns the first `maxEntries` rows and whether the level had more.
|
|
144
|
+
*/
|
|
145
|
+
export function parseLsListing(stdout, parent, maxEntries) {
|
|
146
|
+
const rows = [];
|
|
147
|
+
let truncated = false;
|
|
148
|
+
for (const line of String(stdout).split("\n")) {
|
|
149
|
+
if (!line.endsWith("/")) continue;
|
|
150
|
+
const name = line.slice(0, -1);
|
|
151
|
+
if (name.length === 0) continue;
|
|
152
|
+
if (rows.length === maxEntries) {
|
|
153
|
+
truncated = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
rows.push({ name, path: win32.join(parent, name), hidden: name.startsWith(".") });
|
|
157
|
+
}
|
|
158
|
+
return { rows, truncated };
|
|
159
|
+
}
|