dsh-over-ssh 0.2.1 → 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/README.md +12 -3
- package/agents/agent-manifest.json +4 -4
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +111 -40
- package/lib/file-references.js +17 -0
- package/lib/fs.js +87 -18
- package/lib/index.js +550 -145
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +11 -2
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +8 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +9 -3
- package/lib/types/src/host/integration/session/session-header.d.ts +200 -0
- package/lib/types/src/wiring/helpers.d.ts +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -420,7 +420,7 @@ function createProxyForwarder(options) {
|
|
|
420
420
|
}
|
|
421
421
|
const idleMs = now() - current.lastActivityAt;
|
|
422
422
|
if (idleMs < idleTimeoutMs) return;
|
|
423
|
-
void closeEntry(current).then(() => options.onReclaimed?.(handle, idleMs));
|
|
423
|
+
void closeEntry(current).then(() => options.onReclaimed?.(handle, idleMs)).catch(() => void 0);
|
|
424
424
|
}, checkMs);
|
|
425
425
|
entry.timer.unref?.();
|
|
426
426
|
}
|
|
@@ -1267,6 +1267,13 @@ ${buildRemoteScript(request.script)}`;
|
|
|
1267
1267
|
...request.env === void 0 ? {} : { env: request.env }
|
|
1268
1268
|
});
|
|
1269
1269
|
}
|
|
1270
|
+
/** Mark the transport lost **only if the dead session is still the current one** (a reconnect may have won). */
|
|
1271
|
+
#markSessionLost(session) {
|
|
1272
|
+
if (this.#session !== session) return;
|
|
1273
|
+
this.#sftp = void 0;
|
|
1274
|
+
this.#sftpPending = void 0;
|
|
1275
|
+
this.markState("lost");
|
|
1276
|
+
}
|
|
1270
1277
|
#requireSession() {
|
|
1271
1278
|
if (this.#session === void 0) {
|
|
1272
1279
|
throw new SshDevError("CONNECTION_LOST", "SSH \u4F1A\u8BDD\u5C1A\u672A\u5EFA\u7ACB", { connectionId: this.id });
|
|
@@ -1324,8 +1331,13 @@ ${buildRemoteScript(request.script)}`;
|
|
|
1324
1331
|
}
|
|
1325
1332
|
const connector = this.#options.connector ?? defaultConnector;
|
|
1326
1333
|
try {
|
|
1327
|
-
|
|
1334
|
+
const session = await connector(signal === void 0 ? connect2 : { ...connect2, signal });
|
|
1335
|
+
this.#session = session;
|
|
1328
1336
|
this.#sessionCount++;
|
|
1337
|
+
const onLost = () => this.#markSessionLost(session);
|
|
1338
|
+
session.on?.("close", onLost);
|
|
1339
|
+
session.on?.("error", onLost);
|
|
1340
|
+
session.on?.("end", onLost);
|
|
1329
1341
|
this.markState("ready");
|
|
1330
1342
|
} catch (error) {
|
|
1331
1343
|
this.markState("lost");
|
|
@@ -1651,6 +1663,35 @@ var TransportService = class {
|
|
|
1651
1663
|
|
|
1652
1664
|
// src/host/integration/session-scope.ts
|
|
1653
1665
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1666
|
+
|
|
1667
|
+
// src/host/integration/session-fork-pending.ts
|
|
1668
|
+
var pending = /* @__PURE__ */ new Map();
|
|
1669
|
+
var MAX_PENDING = 256;
|
|
1670
|
+
function markForkInheritancePending(childSessionId, sourceSessionId) {
|
|
1671
|
+
const child = childSessionId.trim();
|
|
1672
|
+
const source = sourceSessionId.trim();
|
|
1673
|
+
if (child === "" || source === "" || child === source) return;
|
|
1674
|
+
pending.delete(child);
|
|
1675
|
+
pending.set(child, source);
|
|
1676
|
+
while (pending.size > MAX_PENDING) {
|
|
1677
|
+
const oldest = pending.keys().next();
|
|
1678
|
+
if (oldest.done === true) break;
|
|
1679
|
+
pending.delete(oldest.value);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function pendingForkSources(sessionId) {
|
|
1683
|
+
if (sessionId === void 0 || sessionId === "") return [];
|
|
1684
|
+
const source = pending.get(sessionId);
|
|
1685
|
+
return source === void 0 ? [] : [source];
|
|
1686
|
+
}
|
|
1687
|
+
function clearForkInheritancePending(childSessionId) {
|
|
1688
|
+
pending.delete(childSessionId);
|
|
1689
|
+
}
|
|
1690
|
+
function clearAllForkInheritancePending() {
|
|
1691
|
+
pending.clear();
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/host/integration/session-scope.ts
|
|
1654
1695
|
var STORE_KEY = /* @__PURE__ */ Symbol.for("dsh-over-ssh/session-scope");
|
|
1655
1696
|
function store() {
|
|
1656
1697
|
const holder = globalThis;
|
|
@@ -1670,6 +1711,15 @@ var MAX_FORK_DEPTH = 32;
|
|
|
1670
1711
|
var ancestorCache = /* @__PURE__ */ new Map();
|
|
1671
1712
|
var ANCESTOR_CACHE_MAX = 256;
|
|
1672
1713
|
function readSessionAncestors(ctx, sessionId) {
|
|
1714
|
+
const pending2 = pendingForkSources(sessionId);
|
|
1715
|
+
if (pending2.length > 0) {
|
|
1716
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
1717
|
+
for (const source of pending2) {
|
|
1718
|
+
inherited.add(source);
|
|
1719
|
+
for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
|
|
1720
|
+
}
|
|
1721
|
+
return [...inherited];
|
|
1722
|
+
}
|
|
1673
1723
|
const cached2 = ancestorCache.get(sessionId);
|
|
1674
1724
|
if (cached2 !== void 0) {
|
|
1675
1725
|
ancestorCache.delete(sessionId);
|
|
@@ -2193,6 +2243,7 @@ var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
|
2193
2243
|
var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
|
|
2194
2244
|
var MAX_EXEC_TIMEOUT_MS = 3e5;
|
|
2195
2245
|
var MAX_EXEC_ENV_ENTRIES = 64;
|
|
2246
|
+
var MAX_EXEC_BATCH_CONNECTIONS = 8;
|
|
2196
2247
|
var ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2197
2248
|
function stringArg(value) {
|
|
2198
2249
|
if (typeof value !== "string") return void 0;
|
|
@@ -3016,16 +3067,16 @@ function createTransferService(dependencies) {
|
|
|
3016
3067
|
};
|
|
3017
3068
|
const partialBytesOnRemote = async (absolute, context, signal) => {
|
|
3018
3069
|
try {
|
|
3019
|
-
const
|
|
3020
|
-
return
|
|
3070
|
+
const stat7 = await dependencies.statRemote(absolute);
|
|
3071
|
+
return stat7.exists && stat7.isFile ? stat7.size : void 0;
|
|
3021
3072
|
} catch (error) {
|
|
3022
3073
|
throw failureFrom(error, "remote-stat-failed", "\u8BFB\u53D6\u8FDC\u7AEF\u76EE\u6807\u4FE1\u606F\u5931\u8D25\uFF08\u7EED\u4F20\u524D\u9700\u8981\u786E\u8BA4\u5DF2\u6709\u957F\u5EA6\uFF09", { ...context }, signal);
|
|
3023
3074
|
}
|
|
3024
3075
|
};
|
|
3025
3076
|
const partialBytesOnLocal = async (path, context, signal) => {
|
|
3026
3077
|
try {
|
|
3027
|
-
const
|
|
3028
|
-
return
|
|
3078
|
+
const stat7 = await dependencies.statLocal(path);
|
|
3079
|
+
return stat7.exists && stat7.isFile ? stat7.size : void 0;
|
|
3029
3080
|
} catch (error) {
|
|
3030
3081
|
throw failureFrom(error, "local-stat-failed", "\u8BFB\u53D6\u672C\u5730\u76EE\u6807\u4FE1\u606F\u5931\u8D25\uFF08\u7EED\u4F20\u524D\u9700\u8981\u786E\u8BA4\u5DF2\u6709\u957F\u5EA6\uFF09", { ...context }, signal);
|
|
3031
3082
|
}
|
|
@@ -4494,13 +4545,14 @@ function createSessionAdminContext(deps) {
|
|
|
4494
4545
|
const dir = joinPath(joinPath(sessionsRoot, project), session.name);
|
|
4495
4546
|
const contents = await deps.listDir(dir);
|
|
4496
4547
|
let bytes = 0;
|
|
4497
|
-
let updatedAtMs = session.mtimeMs;
|
|
4548
|
+
let updatedAtMs = Math.trunc(session.mtimeMs);
|
|
4498
4549
|
for (const item of contents) {
|
|
4499
4550
|
if (item.isDirectory) continue;
|
|
4500
4551
|
bytes += item.bytes;
|
|
4501
|
-
|
|
4552
|
+
const itemMs = Math.trunc(item.mtimeMs);
|
|
4553
|
+
if (itemMs > updatedAtMs) updatedAtMs = itemMs;
|
|
4502
4554
|
}
|
|
4503
|
-
const header = await deps.readHeader(dir);
|
|
4555
|
+
const header = await deps.readHeader(dir, contents);
|
|
4504
4556
|
if (header === void 0) {
|
|
4505
4557
|
return {
|
|
4506
4558
|
id: session.name,
|
|
@@ -4803,7 +4855,6 @@ function createSessionAdminService(deps) {
|
|
|
4803
4855
|
const ordered = [...entries].sort((left, right) => left.deletedAtMs - right.deletedAtMs);
|
|
4804
4856
|
const doomed = [];
|
|
4805
4857
|
const nowMs = deps.now();
|
|
4806
|
-
let keptBytes = ordered.reduce((sum, item) => sum + item.bytes, 0);
|
|
4807
4858
|
if (policy.maxAgeMs > 0) {
|
|
4808
4859
|
for (const entry of ordered) {
|
|
4809
4860
|
if (nowMs - entry.deletedAtMs > policy.maxAgeMs) doomed.push(entry);
|
|
@@ -4812,6 +4863,7 @@ function createSessionAdminService(deps) {
|
|
|
4812
4863
|
const doomedIds = new Set(doomed.map((entry) => entry.id));
|
|
4813
4864
|
const survivors = ordered.filter((entry) => !doomedIds.has(entry.id));
|
|
4814
4865
|
if (policy.maxBytes > 0) {
|
|
4866
|
+
let keptBytes = survivors.reduce((sum, item) => sum + item.bytes, 0);
|
|
4815
4867
|
for (const entry of survivors) {
|
|
4816
4868
|
if (keptBytes <= policy.maxBytes) break;
|
|
4817
4869
|
doomed.push(entry);
|
|
@@ -4824,7 +4876,7 @@ function createSessionAdminService(deps) {
|
|
|
4824
4876
|
const excess = stillKept.length - policy.maxEntries;
|
|
4825
4877
|
for (const entry of stillKept.slice(0, excess)) {
|
|
4826
4878
|
doomed.push(entry);
|
|
4827
|
-
|
|
4879
|
+
doomedIds.add(entry.id);
|
|
4828
4880
|
}
|
|
4829
4881
|
}
|
|
4830
4882
|
const outcome = await purgeEntries(doomed, entries);
|
|
@@ -4840,11 +4892,54 @@ function createSessionAdminService(deps) {
|
|
|
4840
4892
|
}
|
|
4841
4893
|
|
|
4842
4894
|
// src/host/integration/session/session-header.ts
|
|
4843
|
-
import { open, readdir } from "node:fs/promises";
|
|
4895
|
+
import { open, readdir, stat } from "node:fs/promises";
|
|
4844
4896
|
import { join } from "node:path";
|
|
4845
4897
|
import { createZstdDecompress } from "node:zlib";
|
|
4846
4898
|
var SESSION_HEADER_READ_BYTES = 64 * 1024;
|
|
4847
4899
|
var SESSION_HEADER_RETRY_BYTES = 1024 * 1024;
|
|
4900
|
+
function createCachedSessionHeaderReader(io, options = {}) {
|
|
4901
|
+
const inner = createSessionHeaderReader(io);
|
|
4902
|
+
const stat7 = io.stat;
|
|
4903
|
+
if (stat7 === void 0) return inner;
|
|
4904
|
+
const maxEntries = Math.max(1, Math.floor(options.maxEntries ?? SESSION_HEADER_CACHE_MAX));
|
|
4905
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4906
|
+
return async (dir, listing) => {
|
|
4907
|
+
let signature;
|
|
4908
|
+
if (listing !== void 0) {
|
|
4909
|
+
const log = listing.find((entry) => entry.name.startsWith("session.v") && entry.name.endsWith(".zstd"));
|
|
4910
|
+
if (log !== void 0) signature = `${log.size}:${Math.trunc(log.mtimeMs)}`;
|
|
4911
|
+
} else {
|
|
4912
|
+
try {
|
|
4913
|
+
const names = await io.listDir(dir);
|
|
4914
|
+
const log = names.find((name2) => name2.startsWith("session.v") && name2.endsWith(".zstd"));
|
|
4915
|
+
if (log !== void 0) {
|
|
4916
|
+
const info = await stat7(join(dir, log));
|
|
4917
|
+
signature = `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
4918
|
+
}
|
|
4919
|
+
} catch {
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
if (signature !== void 0) {
|
|
4923
|
+
const hit = cache.get(dir);
|
|
4924
|
+
if (hit !== void 0 && hit.signature === signature) {
|
|
4925
|
+
cache.delete(dir);
|
|
4926
|
+
cache.set(dir, hit);
|
|
4927
|
+
return hit.header;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
const header = await inner(dir);
|
|
4931
|
+
if (signature !== void 0) {
|
|
4932
|
+
cache.set(dir, { signature, header });
|
|
4933
|
+
while (cache.size > maxEntries) {
|
|
4934
|
+
const oldest = cache.keys().next();
|
|
4935
|
+
if (oldest.done === true) break;
|
|
4936
|
+
cache.delete(oldest.value);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
return header;
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
var SESSION_HEADER_CACHE_MAX = 2048;
|
|
4848
4943
|
function parseSessionHeaderLine(text8) {
|
|
4849
4944
|
const firstLine = text8.split("\n")[0];
|
|
4850
4945
|
if (firstLine === void 0 || firstLine === "") return void 0;
|
|
@@ -4930,11 +5025,16 @@ function createDiskSessionHeaderIo() {
|
|
|
4930
5025
|
return {
|
|
4931
5026
|
listDir: (dir) => readdir(dir),
|
|
4932
5027
|
readHead: readHeadFromDisk,
|
|
4933
|
-
decompress: decompressPrefix
|
|
5028
|
+
decompress: decompressPrefix,
|
|
5029
|
+
// The cache's only input: `(size, mtime)` is what a session log's append changes.
|
|
5030
|
+
stat: async (path) => {
|
|
5031
|
+
const info = await stat(path);
|
|
5032
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
5033
|
+
}
|
|
4934
5034
|
};
|
|
4935
5035
|
}
|
|
4936
5036
|
function createDiskSessionHeaderReader() {
|
|
4937
|
-
return
|
|
5037
|
+
return createCachedSessionHeaderReader(createDiskSessionHeaderIo());
|
|
4938
5038
|
}
|
|
4939
5039
|
|
|
4940
5040
|
// src/host/app/routed-fs.ts
|
|
@@ -5820,17 +5920,25 @@ var RootRegistry = class {
|
|
|
5820
5920
|
* sessions at all: an empty list means "the session service was not readable", not "no session is live", and
|
|
5821
5921
|
* dropping every grant on a transient read failure would break live turns.
|
|
5822
5922
|
*
|
|
5823
|
-
* @param cwds - working directories of the live sessions.
|
|
5923
|
+
* @param cwds - working directories of the live sessions, optionally with the session id that owns each one.
|
|
5924
|
+
* With an id the root carries `sessions: [id]` and is writable **only by that session**; without one it is a
|
|
5925
|
+
* workspace-scoped root (the legacy shape tests and agentless compositions use).
|
|
5824
5926
|
* @returns the session roots this call added (empty when every cwd was already covered).
|
|
5825
5927
|
*/
|
|
5826
5928
|
syncSessionWorkspaces(cwds) {
|
|
5827
5929
|
if (cwds.length === 0) return [];
|
|
5828
5930
|
const live = /* @__PURE__ */ new Set();
|
|
5829
5931
|
const added = [];
|
|
5830
|
-
for (const
|
|
5932
|
+
for (const item of cwds) {
|
|
5933
|
+
const cwd = typeof item === "string" ? item : item.cwd;
|
|
5934
|
+
const sessionId = typeof item === "string" ? void 0 : item.sessionId;
|
|
5831
5935
|
if (typeof cwd !== "string" || cwd === "") continue;
|
|
5832
5936
|
if (!isAbsoluteLocalPath(normalizeLocalPath2(cwd, { caseInsensitive: false }))) continue;
|
|
5833
|
-
const root = makeLocalRoot(cwd, {
|
|
5937
|
+
const root = makeLocalRoot(cwd, {
|
|
5938
|
+
workspaces: [cwd],
|
|
5939
|
+
...sessionId === void 0 || sessionId === "" ? {} : { sessions: [sessionId] },
|
|
5940
|
+
writable: true
|
|
5941
|
+
});
|
|
5834
5942
|
const key = rootKey(root);
|
|
5835
5943
|
live.add(key);
|
|
5836
5944
|
if (this.#localRootCovers(cwd)) continue;
|
|
@@ -6007,7 +6115,7 @@ function carryMissingState(legacy, current, files, dirs) {
|
|
|
6007
6115
|
renameSync(temp, to);
|
|
6008
6116
|
} catch (error) {
|
|
6009
6117
|
rmSync(temp, { force: true });
|
|
6010
|
-
failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6118
|
+
if (!existsSync(to)) failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6011
6119
|
}
|
|
6012
6120
|
}
|
|
6013
6121
|
for (const name2 of dirs) {
|
|
@@ -6020,7 +6128,7 @@ function carryMissingState(legacy, current, files, dirs) {
|
|
|
6020
6128
|
renameSync(temp, to);
|
|
6021
6129
|
} catch (error) {
|
|
6022
6130
|
rmSync(temp, { recursive: true, force: true });
|
|
6023
|
-
failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6131
|
+
if (!existsSync(to)) failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6024
6132
|
}
|
|
6025
6133
|
}
|
|
6026
6134
|
return failures;
|
|
@@ -6446,9 +6554,9 @@ var AgentFrameParser = class {
|
|
|
6446
6554
|
*/
|
|
6447
6555
|
#append(chunk) {
|
|
6448
6556
|
if (chunk.byteLength === 0) return;
|
|
6449
|
-
const
|
|
6450
|
-
if (this.#store.byteLength - this.#start <
|
|
6451
|
-
const needed =
|
|
6557
|
+
const pending2 = this.#end - this.#start;
|
|
6558
|
+
if (this.#store.byteLength - this.#start < pending2 + chunk.byteLength) {
|
|
6559
|
+
const needed = pending2 + chunk.byteLength;
|
|
6452
6560
|
if (this.#store.byteLength >= needed) {
|
|
6453
6561
|
this.#store.copyWithin(0, this.#start, this.#end);
|
|
6454
6562
|
} else {
|
|
@@ -6457,7 +6565,7 @@ var AgentFrameParser = class {
|
|
|
6457
6565
|
this.#store = grown;
|
|
6458
6566
|
}
|
|
6459
6567
|
this.#start = 0;
|
|
6460
|
-
this.#end =
|
|
6568
|
+
this.#end = pending2;
|
|
6461
6569
|
}
|
|
6462
6570
|
this.#store.set(chunk, this.#end);
|
|
6463
6571
|
this.#end += chunk.byteLength;
|
|
@@ -7242,9 +7350,9 @@ var AgentProtocolClient = class {
|
|
|
7242
7350
|
failConnection(error) {
|
|
7243
7351
|
if (this.#lost !== void 0) return;
|
|
7244
7352
|
this.#lost = error;
|
|
7245
|
-
const
|
|
7353
|
+
const pending2 = [...this.#pending.values()];
|
|
7246
7354
|
this.#pending.clear();
|
|
7247
|
-
for (const entry of
|
|
7355
|
+
for (const entry of pending2) entry.settle({ error });
|
|
7248
7356
|
for (const channel of this.#blobQueue) channel.fail(error);
|
|
7249
7357
|
this.#blobQueue.length = 0;
|
|
7250
7358
|
this.#blobById.clear();
|
|
@@ -8129,14 +8237,14 @@ var AgentTransport = class extends TransportBase {
|
|
|
8129
8237
|
}
|
|
8130
8238
|
}
|
|
8131
8239
|
if (expectedVersion === void 0) {
|
|
8132
|
-
let
|
|
8240
|
+
let stat7;
|
|
8133
8241
|
try {
|
|
8134
|
-
|
|
8242
|
+
stat7 = asRecord(await client.request("fs.stat", { path: request.path }, this.#requestOptions(void 0, signal)));
|
|
8135
8243
|
} catch (error) {
|
|
8136
8244
|
throw this.#translate(error);
|
|
8137
8245
|
}
|
|
8138
|
-
const exists =
|
|
8139
|
-
const observed = typeof
|
|
8246
|
+
const exists = stat7?.["exists"] === true;
|
|
8247
|
+
const observed = typeof stat7?.["version"] === "string" ? stat7["version"] : void 0;
|
|
8140
8248
|
if (!exists || observed === void 0 || observed !== expected) {
|
|
8141
8249
|
throw new SshDevError("TRANSFER_CONFLICT", "\u8FDC\u7AEF\u6587\u4EF6\u7248\u672C\u4E0E\u8C03\u7528\u65B9\u7ED9\u51FA\u7684\u5B88\u536B\u4E0D\u4E00\u81F4\uFF0C\u5199\u5165\u88AB\u62D2\u7EDD", {
|
|
8142
8250
|
connectionId: this.id,
|
|
@@ -9394,7 +9502,7 @@ function localVersion(info) {
|
|
|
9394
9502
|
init_temp_name();
|
|
9395
9503
|
init_errors();
|
|
9396
9504
|
import { constants } from "node:fs";
|
|
9397
|
-
import { access, chmod, lstat, mkdir, open as open2, readdir as readdir2, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
9505
|
+
import { access, chmod, lstat, mkdir, open as open2, readdir as readdir2, rename, rm, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
9398
9506
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
9399
9507
|
var LIST_STAT_CONCURRENCY = 32;
|
|
9400
9508
|
var LocalBackend = class {
|
|
@@ -9407,12 +9515,12 @@ var LocalBackend = class {
|
|
|
9407
9515
|
this.id = options.id;
|
|
9408
9516
|
this.sandboxMode = options.sandboxMode ?? "workspace-write";
|
|
9409
9517
|
this.#maxReadBytes = options.maxReadBytes ?? 16 * 1024 * 1024;
|
|
9410
|
-
this.#statEntry = options.statEntry ?? ((path) =>
|
|
9518
|
+
this.#statEntry = options.statEntry ?? ((path) => stat2(path));
|
|
9411
9519
|
}
|
|
9412
9520
|
async stat(absolute, signal) {
|
|
9413
9521
|
signal?.throwIfAborted();
|
|
9414
9522
|
try {
|
|
9415
|
-
const info = await
|
|
9523
|
+
const info = await stat2(absolute);
|
|
9416
9524
|
return {
|
|
9417
9525
|
exists: true,
|
|
9418
9526
|
isDirectory: info.isDirectory(),
|
|
@@ -10069,7 +10177,9 @@ function buildListScript(absolute) {
|
|
|
10069
10177
|
`if [ ! -d "$p" ]; then printf 'DSH_ERROR=not-a-dir\\n'; exit 4; fi`,
|
|
10070
10178
|
// GNU find (Linux). `-printf ''` is the cheap capability probe: it fails on BSD/macOS find.
|
|
10071
10179
|
`if find "$p" -maxdepth 1 -printf '' >/dev/null 2>&1; then`,
|
|
10072
|
-
|
|
10180
|
+
// **NUL 分隔**:文件名可以含换行(POSIX 合法,`git checkout` 就能造出来),按行切会把一个条目拆成
|
|
10181
|
+
// 两行、解析出一个不存在的名字并丢掉真条目(审计 2026-11-06 第 3 轮 P1)。
|
|
10182
|
+
` find "$p" -mindepth 1 -maxdepth 1 -printf '%y\\t%Y\\t%s\\t%T@\\t%f\\0'`,
|
|
10073
10183
|
`else`,
|
|
10074
10184
|
` for entry in "$p"/* "$p"/.[!.]*; do`,
|
|
10075
10185
|
` [ -e "$entry" ] || continue`,
|
|
@@ -10077,28 +10187,28 @@ function buildListScript(absolute) {
|
|
|
10077
10187
|
` if [ -d "$entry" ]; then kind=d; elif [ -L "$entry" ]; then kind=l; else kind=f; fi`,
|
|
10078
10188
|
` size=$(wc -c < "$entry" 2>/dev/null | tr -d ' ' || echo 0)`,
|
|
10079
10189
|
` mtime=$(stat -c '%Y' "$entry" 2>/dev/null || stat -f '%m' "$entry" 2>/dev/null || echo 0)`,
|
|
10080
|
-
` printf '%s\\t%s\\t%s\\t%s\\t%s\\
|
|
10190
|
+
` printf '%s\\t%s\\t%s\\t%s\\t%s\\0' "$kind" "$kind" "$size" "$mtime" "$name"`,
|
|
10081
10191
|
` done`,
|
|
10082
10192
|
`fi`
|
|
10083
10193
|
].join("\n");
|
|
10084
10194
|
}
|
|
10085
10195
|
function parseListing(stdout) {
|
|
10086
10196
|
const entries = [];
|
|
10087
|
-
for (const
|
|
10088
|
-
if (
|
|
10089
|
-
const parts =
|
|
10197
|
+
for (const record2 of stdout.split("\0")) {
|
|
10198
|
+
if (record2 === "" || record2.startsWith("DSH_")) continue;
|
|
10199
|
+
const parts = record2.split(" ");
|
|
10090
10200
|
if (parts.length < 5) continue;
|
|
10091
10201
|
const [typeChar, targetChar, sizeText, mtimeText] = parts;
|
|
10092
10202
|
const kind = typeChar === "d" ? "d" : typeChar === "l" ? targetChar === "d" ? "d" : "l" : "f";
|
|
10093
10203
|
const size = Number.parseInt(sizeText, 10);
|
|
10094
|
-
const seconds = Number.
|
|
10204
|
+
const seconds = Number.parseFloat(mtimeText);
|
|
10095
10205
|
entries.push({
|
|
10096
10206
|
name: parts.slice(4).join(" "),
|
|
10097
10207
|
isDirectory: kind === "d",
|
|
10098
10208
|
isFile: kind === "f",
|
|
10099
10209
|
isSymlink: kind === "l",
|
|
10100
10210
|
size: Number.isFinite(size) ? size : 0,
|
|
10101
|
-
mtimeMs:
|
|
10211
|
+
mtimeMs: Number.isFinite(seconds) ? Math.trunc(seconds * 1e3) : 0
|
|
10102
10212
|
});
|
|
10103
10213
|
}
|
|
10104
10214
|
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
@@ -10486,6 +10596,13 @@ var RemoteBackend = class {
|
|
|
10486
10596
|
if (result.stdout.includes("DSH_ERROR=not-a-dir")) {
|
|
10487
10597
|
throw new SshDevError("REMOTE_NOT_FOUND", `\u8FDC\u7AEF\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${absolute}`, { path: absolute });
|
|
10488
10598
|
}
|
|
10599
|
+
if (result.truncated === true) {
|
|
10600
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u76EE\u5F55\u6761\u76EE\u8FC7\u591A\uFF0C\u5217\u4E3E\u7ED3\u679C\u88AB\u622A\u65AD\uFF1A${absolute}`, {
|
|
10601
|
+
path: absolute,
|
|
10602
|
+
reason: "listing-truncated",
|
|
10603
|
+
maxBytes: 4 * 1024 * 1024
|
|
10604
|
+
});
|
|
10605
|
+
}
|
|
10489
10606
|
return parseListing(result.stdout);
|
|
10490
10607
|
}
|
|
10491
10608
|
/**
|
|
@@ -11127,7 +11244,21 @@ var RemoteBackend = class {
|
|
|
11127
11244
|
}
|
|
11128
11245
|
const command = tool === "sha256sum" ? "sha256sum" : "shasum -a 256";
|
|
11129
11246
|
const result = await (await this.#transport()).exec({ argv: ["sh", "-c", `${command} -- "$1" | cut -d' ' -f1`, "dsh-digest", absolute] }, signal);
|
|
11130
|
-
|
|
11247
|
+
if (result.exitCode !== 0) {
|
|
11248
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u65E0\u6CD5\u8BA1\u7B97\u8FDC\u7AEF\u6587\u4EF6\u7684 sha256\uFF08\u6458\u8981\u547D\u4EE4\u9000\u51FA\u7801 ${result.exitCode}\uFF09\uFF1A${absolute}`, {
|
|
11249
|
+
path: absolute,
|
|
11250
|
+
exitCode: result.exitCode,
|
|
11251
|
+
stderr: result.stderr.slice(0, 200)
|
|
11252
|
+
});
|
|
11253
|
+
}
|
|
11254
|
+
const digest = result.stdout.trim();
|
|
11255
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) {
|
|
11256
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u8FDC\u7AEF\u6458\u8981\u5DE5\u5177\u8FD4\u56DE\u4E86\u975E\u6CD5\u6458\u8981\uFF08\u4E0D\u662F 64 \u4F4D\u5C0F\u5199\u5341\u516D\u8FDB\u5236\uFF09\uFF1A${absolute}`, {
|
|
11257
|
+
path: absolute,
|
|
11258
|
+
digest: digest.slice(0, 120)
|
|
11259
|
+
});
|
|
11260
|
+
}
|
|
11261
|
+
return digest;
|
|
11131
11262
|
}
|
|
11132
11263
|
};
|
|
11133
11264
|
function sha256Hex(bytes) {
|
|
@@ -11958,12 +12089,12 @@ init_ssh2_transport();
|
|
|
11958
12089
|
|
|
11959
12090
|
// src/host/integration/agent/agent-install-io.ts
|
|
11960
12091
|
init_shell_quote();
|
|
11961
|
-
import { readFile as readFile2, stat as
|
|
12092
|
+
import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
|
|
11962
12093
|
var DEFAULT_MAX_UPLOAD_BYTES = 32 * 1024 * 1024;
|
|
11963
12094
|
function createAgentInstallIo(options) {
|
|
11964
12095
|
const maxUploadBytes = options.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
11965
12096
|
const readLocal = options.readLocalFile ?? ((path) => readFile2(path).then((buffer) => new Uint8Array(buffer)));
|
|
11966
|
-
const sizeOf = options.localSize ?? ((path) =>
|
|
12097
|
+
const sizeOf = options.localSize ?? ((path) => stat3(path).then((info) => info.size));
|
|
11967
12098
|
const exec = async (command, signal) => {
|
|
11968
12099
|
const result = await options.transport.exec(
|
|
11969
12100
|
{ argv: ["/bin/sh", "-c", command], maxBytes: options.probeMaxBytes ?? 256 * 1024 },
|
|
@@ -12046,14 +12177,14 @@ function reviseToTier1(profile, decision, reason, hint) {
|
|
|
12046
12177
|
// src/host/integration/agent/askpass-registry.ts
|
|
12047
12178
|
function createAskpassRegistry(options) {
|
|
12048
12179
|
const handles = /* @__PURE__ */ new Map();
|
|
12049
|
-
const
|
|
12180
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
12050
12181
|
const retired = /* @__PURE__ */ new Set();
|
|
12051
12182
|
const prepare = async (profile) => {
|
|
12052
12183
|
if (!needsAskpass(profile.auth.kind)) return void 0;
|
|
12053
12184
|
if (retired.has(profile.id)) return void 0;
|
|
12054
12185
|
const existing = handles.get(profile.id);
|
|
12055
12186
|
if (existing !== void 0) return existing.env;
|
|
12056
|
-
const inflight =
|
|
12187
|
+
const inflight = pending2.get(profile.id);
|
|
12057
12188
|
if (inflight !== void 0) return inflight;
|
|
12058
12189
|
const attempt = (async () => {
|
|
12059
12190
|
try {
|
|
@@ -12070,16 +12201,16 @@ function createAskpassRegistry(options) {
|
|
|
12070
12201
|
return void 0;
|
|
12071
12202
|
}
|
|
12072
12203
|
})().finally(() => {
|
|
12073
|
-
|
|
12204
|
+
pending2.delete(profile.id);
|
|
12074
12205
|
});
|
|
12075
|
-
|
|
12206
|
+
pending2.set(profile.id, attempt);
|
|
12076
12207
|
return attempt;
|
|
12077
12208
|
};
|
|
12078
12209
|
const forget = async (connectionId) => {
|
|
12079
12210
|
retired.add(connectionId);
|
|
12080
12211
|
const handle = handles.get(connectionId);
|
|
12081
12212
|
handles.delete(connectionId);
|
|
12082
|
-
|
|
12213
|
+
pending2.delete(connectionId);
|
|
12083
12214
|
if (handle !== void 0) await handle.dispose().catch(() => void 0);
|
|
12084
12215
|
};
|
|
12085
12216
|
return {
|
|
@@ -12097,7 +12228,7 @@ function createAskpassRegistry(options) {
|
|
|
12097
12228
|
retired.delete(connectionId);
|
|
12098
12229
|
},
|
|
12099
12230
|
disposeAll: async () => {
|
|
12100
|
-
const ids = /* @__PURE__ */ new Set([...handles.keys(), ...
|
|
12231
|
+
const ids = /* @__PURE__ */ new Set([...handles.keys(), ...pending2.keys()]);
|
|
12101
12232
|
for (const id of ids) retired.add(id);
|
|
12102
12233
|
await Promise.all([...ids].map((id) => forget(id)));
|
|
12103
12234
|
}
|
|
@@ -13455,12 +13586,8 @@ function wireConnections(ctx, config) {
|
|
|
13455
13586
|
}
|
|
13456
13587
|
function readLiveSessions(ctx) {
|
|
13457
13588
|
let list;
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
list = sessions?.list?.();
|
|
13461
|
-
} catch {
|
|
13462
|
-
return [];
|
|
13463
|
-
}
|
|
13589
|
+
const sessions = ctx.get("sessions");
|
|
13590
|
+
list = sessions?.list?.();
|
|
13464
13591
|
if (!Array.isArray(list)) return [];
|
|
13465
13592
|
const live = [];
|
|
13466
13593
|
for (const session of list) {
|
|
@@ -13482,9 +13609,9 @@ import { bindTypertRemote } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
13482
13609
|
// src/shared/remote-identity.ts
|
|
13483
13610
|
var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
13484
13611
|
var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
13612
|
+
var SSH_DEV_REMOTE_METHOD = "connections";
|
|
13485
13613
|
|
|
13486
13614
|
// src/host/integration/remote/remote-face.ts
|
|
13487
|
-
var SSH_DEV_REMOTE_CONNECTIONS_METHOD = "connections";
|
|
13488
13615
|
var NOT_CONNECTED_NOTE = "\u5C1A\u672A\u5EFA\u7ACB\u8FDE\u63A5\uFF1A\u4F20\u8F93\u51B3\u7B56\u5728\u9996\u6B21\u8FDE\u63A5\u65F6\u786E\u5B9A\u3002";
|
|
13489
13616
|
function buildConnectionStatusRows(source) {
|
|
13490
13617
|
return source.profiles().map((profile) => {
|
|
@@ -13549,7 +13676,7 @@ var SshDevRemoteFace = class extends Service {
|
|
|
13549
13676
|
return buildConnectionStatusRows(this.#source);
|
|
13550
13677
|
}
|
|
13551
13678
|
};
|
|
13552
|
-
markRemoteMethod(SshDevRemoteFace.prototype,
|
|
13679
|
+
markRemoteMethod(SshDevRemoteFace.prototype, SSH_DEV_REMOTE_METHOD);
|
|
13553
13680
|
function installRemoteFace(ctx) {
|
|
13554
13681
|
return new SshDevRemoteFace(ctx, {
|
|
13555
13682
|
profiles: () => ctx.get("sshDevConnections")?.list() ?? [],
|
|
@@ -13753,7 +13880,7 @@ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
|
13753
13880
|
init_errors();
|
|
13754
13881
|
|
|
13755
13882
|
// src/host/app/roots/user-roots.ts
|
|
13756
|
-
import { mkdir as mkdir4, readFile as readFile5, rename as rename3, unlink as unlink3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13883
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename3, stat as stat4, unlink as unlink3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13757
13884
|
import { join as join12 } from "node:path";
|
|
13758
13885
|
init_errors();
|
|
13759
13886
|
var USER_ROOTS_VERSION = 1;
|
|
@@ -13779,6 +13906,10 @@ var nodeUserRootsIo = {
|
|
|
13779
13906
|
await mkdir4(path, { recursive: true, mode: 448 });
|
|
13780
13907
|
},
|
|
13781
13908
|
rename: (from, to) => rename3(from, to),
|
|
13909
|
+
stat: async (path) => {
|
|
13910
|
+
const info = await stat4(path);
|
|
13911
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
13912
|
+
},
|
|
13782
13913
|
unlink: async (path) => {
|
|
13783
13914
|
try {
|
|
13784
13915
|
await unlink3(path);
|
|
@@ -14141,7 +14272,19 @@ function createUserRootsStore(options) {
|
|
|
14141
14272
|
const filePath = join12(options.dir, USER_ROOTS_FILE_NAME);
|
|
14142
14273
|
let cachedRaw;
|
|
14143
14274
|
let cachedLoad;
|
|
14275
|
+
let cachedSignature;
|
|
14276
|
+
const signatureOf = async () => {
|
|
14277
|
+
if (io.stat === void 0) return void 0;
|
|
14278
|
+
try {
|
|
14279
|
+
const info = await io.stat(filePath);
|
|
14280
|
+
return `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
14281
|
+
} catch {
|
|
14282
|
+
return void 0;
|
|
14283
|
+
}
|
|
14284
|
+
};
|
|
14144
14285
|
const load = async () => {
|
|
14286
|
+
const signature = await signatureOf();
|
|
14287
|
+
if (signature !== void 0 && signature === cachedSignature && cachedLoad !== void 0) return cachedLoad;
|
|
14145
14288
|
let text8;
|
|
14146
14289
|
try {
|
|
14147
14290
|
text8 = await io.readFile(filePath);
|
|
@@ -14153,10 +14296,14 @@ function createUserRootsStore(options) {
|
|
|
14153
14296
|
degraded: true
|
|
14154
14297
|
};
|
|
14155
14298
|
}
|
|
14156
|
-
if (cachedRaw !== void 0 && text8 === cachedRaw && cachedLoad !== void 0)
|
|
14299
|
+
if (cachedRaw !== void 0 && text8 === cachedRaw && cachedLoad !== void 0) {
|
|
14300
|
+
cachedSignature = signature;
|
|
14301
|
+
return cachedLoad;
|
|
14302
|
+
}
|
|
14157
14303
|
const parsed = parseUserRoots(text8);
|
|
14158
14304
|
cachedRaw = text8;
|
|
14159
14305
|
cachedLoad = parsed;
|
|
14306
|
+
cachedSignature = signature;
|
|
14160
14307
|
return parsed;
|
|
14161
14308
|
};
|
|
14162
14309
|
const write = async (next, preparedText) => {
|
|
@@ -14173,6 +14320,7 @@ function createUserRootsStore(options) {
|
|
|
14173
14320
|
}
|
|
14174
14321
|
cachedRaw = text8;
|
|
14175
14322
|
cachedLoad = { state: validated, warnings: [], degraded: false };
|
|
14323
|
+
cachedSignature = void 0;
|
|
14176
14324
|
return validated;
|
|
14177
14325
|
};
|
|
14178
14326
|
const queue = createSerialQueue();
|
|
@@ -14404,6 +14552,9 @@ var STATUS_BY_CODE = {
|
|
|
14404
14552
|
ROOT_INVALID: 400,
|
|
14405
14553
|
CONNECTION_INVALID: 400,
|
|
14406
14554
|
CONNECTION_NOT_FOUND: 400,
|
|
14555
|
+
// 路径不在任何已注册的根内 / 根没有注册:客户端换一个路径或先加根就能成功,不是服务器故障。
|
|
14556
|
+
// 它此前漏在表外,于是**所有**经 HTTP 暴露的根外访问都报 500(审计 2026-11-06 第 4 轮 P1)。
|
|
14557
|
+
ROOT_OUTSIDE: 400,
|
|
14407
14558
|
// 目标不存在 / 状态冲突 / 本组合不支持:客户端可以据此换一条路。
|
|
14408
14559
|
TRASH_NOT_FOUND: 404,
|
|
14409
14560
|
TRANSFER_CONFLICT: 409,
|
|
@@ -14446,6 +14597,9 @@ function optionalPath(value) {
|
|
|
14446
14597
|
reason: "invalid-path"
|
|
14447
14598
|
});
|
|
14448
14599
|
}
|
|
14600
|
+
if (/[\u0000-\u001f]/.test(value)) {
|
|
14601
|
+
throw new SshDevError("ROOT_INVALID", "path \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\u542B NUL\uFF09", { reason: "invalid-path" });
|
|
14602
|
+
}
|
|
14449
14603
|
return value.trim();
|
|
14450
14604
|
}
|
|
14451
14605
|
function pick(source, fields) {
|
|
@@ -14492,6 +14646,15 @@ function connectionIdFromAlias(alias) {
|
|
|
14492
14646
|
// src/host/app/roots/session-directories.ts
|
|
14493
14647
|
var SESSION_DIRECTORY_MAX_ROOTS = 32;
|
|
14494
14648
|
var SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14649
|
+
var REMOTE_SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14650
|
+
var REMOTE_BROAD_ROOTS = [...REMOTE_SYSTEM_TREES, "/home", "/opt", "/srv", "/mnt", "/media", "/tmp"];
|
|
14651
|
+
function remotePathTooBroad(normalized) {
|
|
14652
|
+
if (REMOTE_BROAD_ROOTS.includes(normalized)) return true;
|
|
14653
|
+
for (const tree of REMOTE_SYSTEM_TREES) {
|
|
14654
|
+
if (normalized === tree || normalized.startsWith(`${tree}/`)) return true;
|
|
14655
|
+
}
|
|
14656
|
+
return false;
|
|
14657
|
+
}
|
|
14495
14658
|
function directoryRefusal(request) {
|
|
14496
14659
|
const trimmed = request.path.trim();
|
|
14497
14660
|
if (trimmed === "") return "\u76EE\u5F55\u8DEF\u5F84\u4E3A\u7A7A\u3002";
|
|
@@ -14507,6 +14670,9 @@ function directoryRefusal(request) {
|
|
|
14507
14670
|
return error instanceof Error ? error.message : `\u8FDC\u7AEF\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF1A${trimmed}`;
|
|
14508
14671
|
}
|
|
14509
14672
|
if (normalized2 === "/") return "\u4E0D\u5141\u8BB8\u628A\u8FDC\u7AEF\u7684\u6839\u76EE\u5F55 / \u6DFB\u52A0\u5230\u4F1A\u8BDD\u3002";
|
|
14673
|
+
if (remotePathTooBroad(normalized2)) {
|
|
14674
|
+
return `\u4E0D\u5141\u8BB8\u628A\u8FC7\u5BBD\u7684\u8FDC\u7AEF\u76EE\u5F55\u6DFB\u52A0\u5230\u4F1A\u8BDD\uFF08${normalized2}\uFF09\uFF1A\u8BF7\u9009\u5230\u5177\u4F53\u9879\u76EE\u76EE\u5F55\u3002`;
|
|
14675
|
+
}
|
|
14510
14676
|
return void 0;
|
|
14511
14677
|
}
|
|
14512
14678
|
const normalized = trimmed.replace(/\/+$/u, "") || "/";
|
|
@@ -14644,6 +14810,15 @@ async function ensureLiveAndReadAnchor(deps, key, state) {
|
|
|
14644
14810
|
}
|
|
14645
14811
|
return readAnchor(await Promise.resolve(deps.listRoots()));
|
|
14646
14812
|
}
|
|
14813
|
+
async function liveScopeWarning(deps, key, state) {
|
|
14814
|
+
const persisted = findRootInState(state, key);
|
|
14815
|
+
if (persisted === void 0) return void 0;
|
|
14816
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14817
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14818
|
+
if (liveEntry === void 0) return `\u6DFB\u52A0\u76EE\u5F55\uFF1A\u6839 ${key} \u4E0D\u5728\u5B9E\u65F6\u6CE8\u518C\u8868\u91CC\uFF0C\u91CD\u542F\u524D\u53EF\u80FD\u4E0D\u53EF\u7528`;
|
|
14819
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return void 0;
|
|
14820
|
+
return `\u6DFB\u52A0\u76EE\u5F55\uFF1A\u6839 ${key} \u7684\u5B9E\u65F6\u4F5C\u7528\u57DF\u4E0E\u6587\u4EF6\u4E0D\u4E00\u81F4\uFF0C\u53E6\u4E00\u4E2A\u4F1A\u8BDD\u53EF\u80FD\u8981\u91CD\u542F\u540E\u624D\u53EF\u89C1`;
|
|
14821
|
+
}
|
|
14647
14822
|
async function findLiveKey(deps, entry) {
|
|
14648
14823
|
for (const raw of await Promise.resolve(deps.listRoots())) {
|
|
14649
14824
|
const key = liveKeyOfRecord(raw, entry);
|
|
@@ -14910,9 +15085,25 @@ function publicRoot(raw) {
|
|
|
14910
15085
|
...typeof source["connectionId"] === "string" ? { connectionId: source["connectionId"] } : {},
|
|
14911
15086
|
path: String(source["path"] ?? ""),
|
|
14912
15087
|
...typeof source["label"] === "string" ? { label: source["label"] } : {},
|
|
14913
|
-
...typeof source["writable"] === "boolean" ? { writable: source["writable"] } : {}
|
|
15088
|
+
...typeof source["writable"] === "boolean" ? { writable: source["writable"] } : {},
|
|
15089
|
+
...stringList(source["workspaces"]) === void 0 ? {} : { workspaces: stringList(source["workspaces"]) },
|
|
15090
|
+
...stringList(source["sessions"]) === void 0 ? {} : { sessions: stringList(source["sessions"]) }
|
|
14914
15091
|
};
|
|
14915
15092
|
}
|
|
15093
|
+
function liveRootScopes(raw) {
|
|
15094
|
+
const outer = asRecord2(raw) ?? {};
|
|
15095
|
+
const source = asRecord2(outer["root"]) ?? outer;
|
|
15096
|
+
const sessions = stringList(source["sessions"]);
|
|
15097
|
+
const workspaces = stringList(source["workspaces"]);
|
|
15098
|
+
return {
|
|
15099
|
+
...sessions === void 0 ? {} : { sessions },
|
|
15100
|
+
...workspaces === void 0 ? {} : { workspaces }
|
|
15101
|
+
};
|
|
15102
|
+
}
|
|
15103
|
+
function stringList(value) {
|
|
15104
|
+
if (!Array.isArray(value)) return void 0;
|
|
15105
|
+
return value.filter((item) => typeof item === "string");
|
|
15106
|
+
}
|
|
14916
15107
|
function publicConnection(raw) {
|
|
14917
15108
|
const source = asRecord2(raw) ?? {};
|
|
14918
15109
|
const authKind = source["authKind"] ?? asRecord2(source["auth"])?.["kind"];
|
|
@@ -15038,21 +15229,34 @@ async function handleRootActions(action, body, req, res, deps) {
|
|
|
15038
15229
|
if (registeredNow) await deps.unregisterRoot(key);
|
|
15039
15230
|
throw error;
|
|
15040
15231
|
}
|
|
15232
|
+
const warnings = [];
|
|
15233
|
+
if (existing !== void 0) {
|
|
15234
|
+
const warning = await liveScopeWarning(deps, key, state);
|
|
15235
|
+
if (warning !== void 0) warnings.push(warning);
|
|
15236
|
+
}
|
|
15041
15237
|
const anchorPath = await anchorPathOf(deps, entry);
|
|
15042
15238
|
send(req, res, 200, {
|
|
15043
15239
|
ok: true,
|
|
15044
15240
|
action,
|
|
15045
15241
|
state: publicUserState(state),
|
|
15046
|
-
...anchorPath === void 0 ? {} : { anchorPath }
|
|
15242
|
+
...anchorPath === void 0 ? {} : { anchorPath },
|
|
15243
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
15047
15244
|
});
|
|
15048
15245
|
return true;
|
|
15049
|
-
return true;
|
|
15050
15246
|
});
|
|
15051
15247
|
}
|
|
15052
15248
|
case "remove-root": {
|
|
15053
15249
|
const key = requiredKey(body["key"]);
|
|
15054
15250
|
const workspace = optionalWorkspaceName(body["workspace"]);
|
|
15055
15251
|
const session = optionalSessionName(body["session"]);
|
|
15252
|
+
if (session !== void 0) {
|
|
15253
|
+
const current = await currentUserState(deps);
|
|
15254
|
+
const entry = findRootInState(current, key);
|
|
15255
|
+
const owners = entry?.sessions ?? [];
|
|
15256
|
+
if (owners.length > 0 && !owners.includes(session)) {
|
|
15257
|
+
throw new SshDevError("ROOT_INVALID", "\u8FD9\u4E2A\u76EE\u5F55\u662F\u53E6\u4E00\u4E2A\u4F1A\u8BDD\u6DFB\u52A0\u7684\uFF1B\u8BF7\u5230\u6DFB\u52A0\u5B83\u7684\u4F1A\u8BDD\u91CC\u79FB\u9664", { key });
|
|
15258
|
+
}
|
|
15259
|
+
}
|
|
15056
15260
|
const state = await reconcile(deps, (current) => {
|
|
15057
15261
|
if (session !== void 0 && rootAddedBySession(current, key, session)) {
|
|
15058
15262
|
return removeRootFromSession(current, key, session);
|
|
@@ -15734,16 +15938,40 @@ async function serveSnapshot(req, res, deps) {
|
|
|
15734
15938
|
Promise.resolve(deps.listConnections()),
|
|
15735
15939
|
Promise.resolve(deps.loadUserState())
|
|
15736
15940
|
]);
|
|
15941
|
+
const query = queryOf(req.url);
|
|
15942
|
+
const requestedSession = (query.get("session") ?? "").trim();
|
|
15943
|
+
const requestedWorkspace = (query.get("workspace") ?? "").trim();
|
|
15944
|
+
const ancestorsOf = deps.resolveSessionAncestors;
|
|
15945
|
+
const canFilter = requestedWorkspace !== "" || ancestorsOf !== void 0;
|
|
15946
|
+
const filtered = !canFilter || requestedSession === "" && requestedWorkspace === "" ? void 0 : {
|
|
15947
|
+
ids: new Set(
|
|
15948
|
+
requestedSession === "" ? [] : [requestedSession, ...ancestorsOf?.(requestedSession) ?? []]
|
|
15949
|
+
),
|
|
15950
|
+
workspace: requestedWorkspace
|
|
15951
|
+
};
|
|
15952
|
+
const roots = filtered === void 0 ? liveRoots.map(publicRoot) : liveRoots.filter((raw) => visibleToSession(liveRootScopes(raw), filtered.ids, filtered.workspace)).map(publicRoot);
|
|
15737
15953
|
send(req, res, 200, {
|
|
15738
15954
|
ok: true,
|
|
15955
|
+
// 两个标记分开写:客户端只认 `scopedForSession`,而只带 workspace 的调用方也能看出"这份列表被过滤过"。
|
|
15956
|
+
...filtered === void 0 ? {} : requestedSession === "" ? { scopedForWorkspace: requestedWorkspace } : { scopedForSession: requestedSession },
|
|
15739
15957
|
// The plugin's **anchor root**, not the roots' individual anchor paths: see `RootsApiDeps.anchorRootDir`.
|
|
15740
15958
|
...deps.anchorRootDir === void 0 ? {} : { anchorRoot: deps.anchorRootDir },
|
|
15741
|
-
roots
|
|
15959
|
+
roots,
|
|
15742
15960
|
connections: liveConnections.map(publicConnection),
|
|
15743
15961
|
userRoots: publicUserState(loaded.state),
|
|
15744
15962
|
warnings: [...loaded.warnings]
|
|
15745
15963
|
});
|
|
15746
15964
|
}
|
|
15965
|
+
function queryOf(url) {
|
|
15966
|
+
const index = url === void 0 ? -1 : url.indexOf("?");
|
|
15967
|
+
return new URLSearchParams(index === -1 || url === void 0 ? "" : url.slice(index + 1));
|
|
15968
|
+
}
|
|
15969
|
+
function visibleToSession(row, sessionIds, workspace) {
|
|
15970
|
+
const sessions = row.sessions ?? [];
|
|
15971
|
+
if (sessions.length > 0) return sessions.some((id) => sessionIds.has(id));
|
|
15972
|
+
if (workspace === "") return false;
|
|
15973
|
+
return (row.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, workspace));
|
|
15974
|
+
}
|
|
15747
15975
|
async function serveAction(req, res, deps) {
|
|
15748
15976
|
let text8;
|
|
15749
15977
|
try {
|
|
@@ -15799,7 +16027,7 @@ async function serveAction(req, res, deps) {
|
|
|
15799
16027
|
|
|
15800
16028
|
// src/host/integration/session/session-bin-lock.ts
|
|
15801
16029
|
init_errors();
|
|
15802
|
-
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as
|
|
16030
|
+
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as stat5 } from "node:fs/promises";
|
|
15803
16031
|
import { dirname as dirname5 } from "node:path";
|
|
15804
16032
|
var DEFAULT_STALE_LOCK_MS = 6e4;
|
|
15805
16033
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
@@ -15857,21 +16085,35 @@ function createSessionBinLock(options) {
|
|
|
15857
16085
|
const reclaimIfStale = async () => {
|
|
15858
16086
|
if (staleMs <= 0) return false;
|
|
15859
16087
|
let age;
|
|
16088
|
+
let malformed = false;
|
|
15860
16089
|
try {
|
|
15861
|
-
const info = await
|
|
16090
|
+
const info = await stat5(options.path);
|
|
16091
|
+
malformed = info.isDirectory();
|
|
15862
16092
|
age = now() - info.mtimeMs;
|
|
15863
16093
|
} catch {
|
|
15864
16094
|
return true;
|
|
15865
16095
|
}
|
|
15866
|
-
if (age < staleMs) return false;
|
|
15867
|
-
|
|
15868
|
-
|
|
15869
|
-
|
|
15870
|
-
|
|
15871
|
-
|
|
16096
|
+
if (!malformed && age < staleMs) return false;
|
|
16097
|
+
if (!malformed) {
|
|
16098
|
+
try {
|
|
16099
|
+
const text8 = await readFile6(options.path, "utf8");
|
|
16100
|
+
const record2 = JSON.parse(text8);
|
|
16101
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: record2 });
|
|
16102
|
+
} catch {
|
|
16103
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: {} });
|
|
16104
|
+
}
|
|
16105
|
+
} else {
|
|
15872
16106
|
options.onReclaim?.({ path: options.path, ageMs: age, holder: {} });
|
|
15873
16107
|
}
|
|
15874
|
-
|
|
16108
|
+
try {
|
|
16109
|
+
await rm3(options.path, { force: true, recursive: malformed });
|
|
16110
|
+
} catch (error) {
|
|
16111
|
+
throw new SshDevError("TRANSFER_CONFLICT", `\u4F1A\u8BDD\u56DE\u6536\u7AD9\u9501\u4F4D\u7F6E\u662F\u4E00\u4E2A\u76EE\u5F55\uFF0C\u4E14\u65E0\u6CD5\u5220\u9664\uFF1A${options.path}`, {
|
|
16112
|
+
path: options.path,
|
|
16113
|
+
reason: "malformed-lock",
|
|
16114
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
16115
|
+
});
|
|
16116
|
+
}
|
|
15875
16117
|
return true;
|
|
15876
16118
|
};
|
|
15877
16119
|
const withLock = async (_key, operation) => {
|
|
@@ -15999,6 +16241,7 @@ function createRootsWiring(deps) {
|
|
|
15999
16241
|
deps.removeConnection(id);
|
|
16000
16242
|
},
|
|
16001
16243
|
...deps.anchorRootDir === void 0 ? {} : { anchorRootDir: deps.anchorRootDir },
|
|
16244
|
+
...deps.resolveSessionAncestors === void 0 ? {} : { resolveSessionAncestors: deps.resolveSessionAncestors },
|
|
16002
16245
|
...listRemoteDirectory === void 0 ? {} : { listRemoteDirectory: (input) => listRemoteDirectory(input) },
|
|
16003
16246
|
...remoteHomeDirectory === void 0 ? {} : { remoteHomeDirectory },
|
|
16004
16247
|
...listLocalDirectory === void 0 ? {} : { listLocalDirectory: (input) => listLocalDirectory(input) },
|
|
@@ -16072,10 +16315,55 @@ function createRootsWiring(deps) {
|
|
|
16072
16315
|
}
|
|
16073
16316
|
return { roots, connections, warnings };
|
|
16074
16317
|
};
|
|
16318
|
+
const inheritAddedRoots = async (sourceSessionId, childSessionId) => {
|
|
16319
|
+
const source = sourceSessionId.trim();
|
|
16320
|
+
const child = childSessionId.trim();
|
|
16321
|
+
if (source === "" || child === "") return;
|
|
16322
|
+
const keys = [];
|
|
16323
|
+
await store2.mutate((current) => {
|
|
16324
|
+
let next = current;
|
|
16325
|
+
for (const entry of current.roots) {
|
|
16326
|
+
const sessions = entry.sessions ?? [];
|
|
16327
|
+
if (!sessions.includes(source) || sessions.includes(child)) continue;
|
|
16328
|
+
const key = userRootKey(entry);
|
|
16329
|
+
next = withSessions(next, key, [child]);
|
|
16330
|
+
keys.push(key);
|
|
16331
|
+
}
|
|
16332
|
+
return next;
|
|
16333
|
+
});
|
|
16334
|
+
if (keys.length > 0) {
|
|
16335
|
+
const { state } = await store2.load();
|
|
16336
|
+
for (const key of keys) await refreshLiveScope(apiDeps, key, state);
|
|
16337
|
+
}
|
|
16338
|
+
};
|
|
16339
|
+
const pruneSession = async (sessionId) => {
|
|
16340
|
+
const trimmed = sessionId.trim();
|
|
16341
|
+
if (trimmed === "") return { changed: [], removed: [] };
|
|
16342
|
+
const before = (await store2.load()).state;
|
|
16343
|
+
const touched = before.roots.filter((entry) => (entry.sessions ?? []).includes(trimmed));
|
|
16344
|
+
if (touched.length === 0) return { changed: [], removed: [] };
|
|
16345
|
+
const after = await store2.mutate((current) => pruneSessionFromRoots(current, trimmed));
|
|
16346
|
+
const afterKeys = new Set(after.roots.map(userRootKey));
|
|
16347
|
+
const removed = [];
|
|
16348
|
+
const changed = [];
|
|
16349
|
+
for (const entry of touched) {
|
|
16350
|
+
const key = userRootKey(entry);
|
|
16351
|
+
if (!afterKeys.has(key)) {
|
|
16352
|
+
removed.push(key);
|
|
16353
|
+
await deps.unregisterRoot(key);
|
|
16354
|
+
} else {
|
|
16355
|
+
changed.push(key);
|
|
16356
|
+
}
|
|
16357
|
+
}
|
|
16358
|
+
for (const key of changed) await refreshLiveScope(apiDeps, key, after);
|
|
16359
|
+
return { changed, removed };
|
|
16360
|
+
};
|
|
16075
16361
|
return {
|
|
16076
16362
|
api,
|
|
16077
16363
|
store: store2,
|
|
16078
16364
|
sessionDirectories,
|
|
16365
|
+
inheritAddedRoots,
|
|
16366
|
+
pruneSession,
|
|
16079
16367
|
mergeUserRoots,
|
|
16080
16368
|
routeWarnings: () => [...routeWarnings],
|
|
16081
16369
|
routeFacts: () => ({ injectRan, routeRegistered, warnings: [...routeWarnings] }),
|
|
@@ -16352,7 +16640,11 @@ async function serveActionWithSignal(action, body, req, res, deps, service, sign
|
|
|
16352
16640
|
return;
|
|
16353
16641
|
}
|
|
16354
16642
|
const result = await service.purge(entryId, options);
|
|
16355
|
-
send(req, res, 200, {
|
|
16643
|
+
send(req, res, 200, {
|
|
16644
|
+
...result.indexStale === true ? { ok: false, error: "\u56DE\u6536\u7AD9\u7D22\u5F15\u66F4\u65B0\u5931\u8D25\uFF1A\u6761\u76EE\u5DF2\u5220\u9664\uFF0C\u4F46\u7D22\u5F15\u4ECD\u5217\u7740\u5B83\uFF08\u4E0B\u6B21\u8FD8\u539F\u4F1A\u5931\u8D25\uFF09\u3002\u91CD\u542F\u540E\u91CD\u8BD5 sweep\u3002", reason: "index-stale" } : { ok: true },
|
|
16645
|
+
purged: result.purged,
|
|
16646
|
+
indexStale: result.indexStale
|
|
16647
|
+
});
|
|
16356
16648
|
return;
|
|
16357
16649
|
}
|
|
16358
16650
|
case "sweep": {
|
|
@@ -16365,8 +16657,9 @@ async function serveActionWithSignal(action, body, req, res, deps, service, sign
|
|
|
16365
16657
|
return;
|
|
16366
16658
|
}
|
|
16367
16659
|
const result = await service.sweep(deps.policy(), options);
|
|
16660
|
+
const stale = result.purged.filter((item) => item.indexStale === true).length;
|
|
16368
16661
|
send(req, res, 200, {
|
|
16369
|
-
ok: true,
|
|
16662
|
+
...stale > 0 ? { ok: false, error: `\u56DE\u6536\u7AD9\u7D22\u5F15\u66F4\u65B0\u5931\u8D25\uFF1A${stale} \u4E2A\u6761\u76EE\u5DF2\u5220\u9664\u4F46\u7D22\u5F15\u4ECD\u5217\u7740\u5B83\u4EEC\uFF08\u4E0B\u6B21\u8FD8\u539F\u4F1A\u5931\u8D25\uFF09\u3002`, reason: "index-stale" } : { ok: true },
|
|
16370
16663
|
purged: result.purged.map((item) => ({ entryId: String(item.id), sessionId: item.sessionId, indexStale: item.indexStale })),
|
|
16371
16664
|
failed: result.failed.map((item) => ({ entryId: String(item.id), reason: item.reason })),
|
|
16372
16665
|
remainingEntries: result.remainingEntries,
|
|
@@ -16441,6 +16734,56 @@ function createSessionRouteWiring(deps) {
|
|
|
16441
16734
|
};
|
|
16442
16735
|
}
|
|
16443
16736
|
|
|
16737
|
+
// src/host/integration/roots/fork-inheritance.ts
|
|
16738
|
+
var PATCHED = /* @__PURE__ */ Symbol.for("dsh-over-ssh/fork-inheritance");
|
|
16739
|
+
function sourceIdOf(source) {
|
|
16740
|
+
if (typeof source === "string") return source === "" ? void 0 : source;
|
|
16741
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
16742
|
+
const candidate = source;
|
|
16743
|
+
const id = candidate.header?.id ?? candidate.id;
|
|
16744
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
16745
|
+
}
|
|
16746
|
+
function installForkInheritance(ctx, deps) {
|
|
16747
|
+
const service = ctx.get("sessions");
|
|
16748
|
+
const original = service?.fork;
|
|
16749
|
+
if (typeof service !== "object" || service === null || typeof original !== "function") {
|
|
16750
|
+
return { installed: false, dispose: () => void 0 };
|
|
16751
|
+
}
|
|
16752
|
+
if (original[PATCHED] === true) {
|
|
16753
|
+
return { installed: true, dispose: () => void 0 };
|
|
16754
|
+
}
|
|
16755
|
+
const invoke = original;
|
|
16756
|
+
const patched2 = function patchedFork(...args) {
|
|
16757
|
+
const child = invoke.apply(this, args);
|
|
16758
|
+
try {
|
|
16759
|
+
const sourceId = sourceIdOf(args[0]);
|
|
16760
|
+
const childId = sourceIdOf(child);
|
|
16761
|
+
if (sourceId !== void 0 && childId !== void 0) {
|
|
16762
|
+
markForkInheritancePending(childId, sourceId);
|
|
16763
|
+
void deps.inherit(sourceId, childId).catch((error) => {
|
|
16764
|
+
deps.warn?.(`fork \u7EE7\u627F\u6DFB\u52A0\u76EE\u5F55\u5931\u8D25\uFF08source=${sourceId} child=${childId}\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16765
|
+
}).finally(() => {
|
|
16766
|
+
clearForkInheritancePending(childId);
|
|
16767
|
+
});
|
|
16768
|
+
}
|
|
16769
|
+
} catch (error) {
|
|
16770
|
+
deps.warn?.(`fork \u7EE7\u627F\u6DFB\u52A0\u76EE\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16771
|
+
}
|
|
16772
|
+
return child;
|
|
16773
|
+
};
|
|
16774
|
+
patched2[PATCHED] = true;
|
|
16775
|
+
service.fork = patched2;
|
|
16776
|
+
return {
|
|
16777
|
+
installed: true,
|
|
16778
|
+
dispose: () => {
|
|
16779
|
+
if (service.fork === patched2) {
|
|
16780
|
+
service.fork = original;
|
|
16781
|
+
}
|
|
16782
|
+
clearAllForkInheritancePending();
|
|
16783
|
+
}
|
|
16784
|
+
};
|
|
16785
|
+
}
|
|
16786
|
+
|
|
16444
16787
|
// src/wiring/fs-panel-routes.ts
|
|
16445
16788
|
function readOnlySeams(env) {
|
|
16446
16789
|
const { ctx, profiles, registry, transports } = env;
|
|
@@ -16658,6 +17001,8 @@ function registerPanelRoutes(env, hooks) {
|
|
|
16658
17001
|
stateDir: resolveStateDir(),
|
|
16659
17002
|
// Published in the GET snapshot so a client can recognise a session whose cwd is a remote workspace.
|
|
16660
17003
|
anchorRootDir: env.anchorRootDir,
|
|
17004
|
+
// fork 继承父会话添加的目录(ADR-0013):血缘只有宿主知道,所以 `GET /roots?session=` 由这里回答。
|
|
17005
|
+
resolveSessionAncestors: (sessionId) => readSessionAncestors(env.ctx, sessionId),
|
|
16661
17006
|
...readOnlySeams(env),
|
|
16662
17007
|
...rootLifecycleSeams(env, hooks),
|
|
16663
17008
|
...connectionLifecycleSeams(env, hooks),
|
|
@@ -16665,7 +17010,11 @@ function registerPanelRoutes(env, hooks) {
|
|
|
16665
17010
|
...remoteFileSeams(env),
|
|
16666
17011
|
webServerProvider: ctx
|
|
16667
17012
|
});
|
|
16668
|
-
|
|
17013
|
+
const forkInheritance = installForkInheritance(ctx, {
|
|
17014
|
+
inherit: (sourceSessionId, childSessionId) => rootsWiring.inheritAddedRoots(sourceSessionId, childSessionId),
|
|
17015
|
+
warn: (message) => ctx.logger?.warn?.(`over-ssh: ${message}`)
|
|
17016
|
+
});
|
|
17017
|
+
return { sessionWiring, rootsWiring, disposeForkInheritance: () => forkInheritance.dispose() };
|
|
16669
17018
|
}
|
|
16670
17019
|
|
|
16671
17020
|
// src/wiring/fs.ts
|
|
@@ -16706,7 +17055,9 @@ function provideAgentStatus(env) {
|
|
|
16706
17055
|
}
|
|
16707
17056
|
};
|
|
16708
17057
|
const forgetPerConnection = (connectionId) => {
|
|
16709
|
-
void forgetPerConnectionAsync(connectionId)
|
|
17058
|
+
void forgetPerConnectionAsync(connectionId).catch((error) => {
|
|
17059
|
+
ctx.logger?.warn?.(`over-ssh: \u5FD8\u8BB0\u8FDE\u63A5 ${connectionId} \u7684\u4EE3\u7406\u5B89\u88C5\u8BB0\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17060
|
+
});
|
|
16710
17061
|
};
|
|
16711
17062
|
const disposeAgent = ctx.provide("sshDevAgent", {
|
|
16712
17063
|
outcome: (connectionId) => agentBridge.installationFor(connectionId),
|
|
@@ -16739,7 +17090,9 @@ function registerConfiguredRoots(env) {
|
|
|
16739
17090
|
ctx.logger?.warn?.(`over-ssh: \u8FDC\u7AEF\u6839\u7684\u951A\u70B9\u76EE\u5F55\u65E0\u6CD5\u521B\u5EFA\uFF08${root.anchorPath ?? ""}\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16740
17091
|
});
|
|
16741
17092
|
}
|
|
16742
|
-
void Promise.allSettled(binRootPreparation).then(runBinHousekeeping)
|
|
17093
|
+
void Promise.allSettled(binRootPreparation).then(runBinHousekeeping).catch((error) => {
|
|
17094
|
+
ctx.logger?.warn?.(`over-ssh: \u56DE\u6536\u7AD9\u6E05\u7406\u7531\u672C\u6B21\u542F\u52A8\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17095
|
+
});
|
|
16743
17096
|
}
|
|
16744
17097
|
function scheduleActivationDiagnostics(env) {
|
|
16745
17098
|
const { ctx, rootsWiring, sessionWiring } = env;
|
|
@@ -16765,7 +17118,7 @@ function scheduleActivationDiagnostics(env) {
|
|
|
16765
17118
|
function watchSessionRemovals(env) {
|
|
16766
17119
|
const { ctx, rootsWiring, onSessionRemoved } = env;
|
|
16767
17120
|
onSessionRemoved((sessionId) => {
|
|
16768
|
-
void rootsWiring.
|
|
17121
|
+
void rootsWiring.pruneSession(sessionId).catch((error) => {
|
|
16769
17122
|
ctx.logger?.warn?.(
|
|
16770
17123
|
`dsh-over-ssh: \u6E05\u7406\u5DF2\u5220\u4F1A\u8BDD ${sessionId} \u7684\u6839\u8BB0\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
16771
17124
|
);
|
|
@@ -16784,6 +17137,7 @@ function createTeardown(env) {
|
|
|
16784
17137
|
const { activationTimers, routes, hostServices, disposeAgent, tools, disposeSessionDirectories } = env;
|
|
16785
17138
|
return async () => {
|
|
16786
17139
|
for (const timer of activationTimers) clearTimeout(timer);
|
|
17140
|
+
routes.disposeForkInheritance();
|
|
16787
17141
|
routes.rootsWiring.dispose();
|
|
16788
17142
|
routes.sessionWiring.dispose();
|
|
16789
17143
|
tools.disposeManagement();
|
|
@@ -16861,7 +17215,7 @@ import { dirname as dirname7, join as join16 } from "node:path";
|
|
|
16861
17215
|
// src/host/integration/transfer/transfer-adapters.ts
|
|
16862
17216
|
init_errors();
|
|
16863
17217
|
import { createHash as createHash3 } from "node:crypto";
|
|
16864
|
-
import { mkdir as mkdir8, open as open4, readFile as readFile8, readdir as readdir5, rename as rename4, rm as rm4, stat as
|
|
17218
|
+
import { mkdir as mkdir8, open as open4, readFile as readFile8, readdir as readdir5, rename as rename4, rm as rm4, stat as stat6, writeFile as writeFile5 } from "node:fs/promises";
|
|
16865
17219
|
import { dirname as dirname6 } from "node:path";
|
|
16866
17220
|
function requireRemote(deps, path) {
|
|
16867
17221
|
const target = deps.routed.resolve(path);
|
|
@@ -16925,7 +17279,7 @@ function createTransferDeps(deps) {
|
|
|
16925
17279
|
},
|
|
16926
17280
|
async statLocal(path) {
|
|
16927
17281
|
try {
|
|
16928
|
-
const info = await
|
|
17282
|
+
const info = await stat6(path);
|
|
16929
17283
|
return toBackendStat(info, localVersion(info));
|
|
16930
17284
|
} catch {
|
|
16931
17285
|
return toBackendStat(void 0, "0:0");
|
|
@@ -17676,7 +18030,7 @@ function createSessionRootIndex(deps) {
|
|
|
17676
18030
|
const now = deps.now ?? (() => Date.now());
|
|
17677
18031
|
let cachedAt;
|
|
17678
18032
|
let cached2 = [];
|
|
17679
|
-
let
|
|
18033
|
+
let pending2;
|
|
17680
18034
|
let generation = 0;
|
|
17681
18035
|
const readEntries = async () => {
|
|
17682
18036
|
try {
|
|
@@ -17690,14 +18044,14 @@ function createSessionRootIndex(deps) {
|
|
|
17690
18044
|
const entriesNow = () => {
|
|
17691
18045
|
const at = cachedAt;
|
|
17692
18046
|
const fresh = at !== void 0 && now() - at < deps.ttlMs;
|
|
17693
|
-
if (!fresh &&
|
|
18047
|
+
if (!fresh && pending2 === void 0) {
|
|
17694
18048
|
const startedAt = generation;
|
|
17695
|
-
|
|
18049
|
+
pending2 = readEntries().then((entries) => {
|
|
17696
18050
|
if (startedAt === generation) {
|
|
17697
18051
|
cached2 = entries;
|
|
17698
18052
|
cachedAt = now();
|
|
17699
18053
|
}
|
|
17700
|
-
|
|
18054
|
+
pending2 = void 0;
|
|
17701
18055
|
return entries;
|
|
17702
18056
|
});
|
|
17703
18057
|
}
|
|
@@ -17712,7 +18066,7 @@ function createSessionRootIndex(deps) {
|
|
|
17712
18066
|
generation += 1;
|
|
17713
18067
|
cachedAt = void 0;
|
|
17714
18068
|
cached2 = [];
|
|
17715
|
-
|
|
18069
|
+
pending2 = void 0;
|
|
17716
18070
|
}
|
|
17717
18071
|
};
|
|
17718
18072
|
}
|
|
@@ -17771,8 +18125,12 @@ function resolveSessionTarget(exec, seams) {
|
|
|
17771
18125
|
function readConnectionIds(record2) {
|
|
17772
18126
|
const raw = record2["connectionIds"];
|
|
17773
18127
|
if (!Array.isArray(raw)) return void 0;
|
|
18128
|
+
if (raw.length > MAX_EXEC_BATCH_CONNECTIONS) {
|
|
18129
|
+
throw new Error(`connectionIds \u4E00\u6B21\u6700\u591A ${MAX_EXEC_BATCH_CONNECTIONS} \u4E2A\uFF08\u6536\u5230 ${raw.length} \u4E2A\uFF09\u3002\u8BF7\u5206\u6279\u6267\u884C\u3002`);
|
|
18130
|
+
}
|
|
17774
18131
|
const ids = [...new Set(raw.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()))];
|
|
17775
|
-
|
|
18132
|
+
if (ids.length === 0) return void 0;
|
|
18133
|
+
return ids;
|
|
17776
18134
|
}
|
|
17777
18135
|
function text2(value) {
|
|
17778
18136
|
return [{ type: "text", text: value }];
|
|
@@ -18239,6 +18597,7 @@ var DEFAULT_JOB_POLL_MS = 500;
|
|
|
18239
18597
|
var WAIT_SLICE_MS = 5e3;
|
|
18240
18598
|
var DEFAULT_JOB_OUTPUT_BYTES = 1 << 20;
|
|
18241
18599
|
var MAX_JOB_OUTPUT_BYTES = 8 << 20;
|
|
18600
|
+
var MAX_JOB_STDIN_BYTES = 1 << 20;
|
|
18242
18601
|
function isAgentJobCapable(transport) {
|
|
18243
18602
|
if (transport === null || typeof transport !== "object") return false;
|
|
18244
18603
|
const candidate = transport;
|
|
@@ -18598,6 +18957,11 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18598
18957
|
const maxOutputBytes = clampInt2(record2["maxOutputBytes"], DEFAULT_JOB_OUTPUT_BYTES, 1, MAX_JOB_OUTPUT_BYTES);
|
|
18599
18958
|
const stdinText = record2["stdin"];
|
|
18600
18959
|
if (stdinText !== void 0 && typeof stdinText !== "string") throw new Error("stdin \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
18960
|
+
if (typeof stdinText === "string" && Buffer.byteLength(stdinText, "utf8") > MAX_JOB_STDIN_BYTES) {
|
|
18961
|
+
throw new Error(
|
|
18962
|
+
`stdin \u4E00\u6B21\u6700\u591A ${MAX_JOB_STDIN_BYTES} \u5B57\u8282\uFF08\u6536\u5230 ${Buffer.byteLength(stdinText, "utf8")}\uFF09\u3002\u8BF7\u7528 interactive:true \u542F\u52A8\uFF0C\u518D\u7528 remote_job {action:"stdin"} \u5206\u5757\u5199\u5165\u3002`
|
|
18963
|
+
);
|
|
18964
|
+
}
|
|
18601
18965
|
const interactive = record2["interactive"] === true;
|
|
18602
18966
|
const target = targetFor2(backend, exec);
|
|
18603
18967
|
const cwd = resolveOrThrow2(target, stringArg2(record2["cwd"]));
|
|
@@ -18717,6 +19081,9 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18717
19081
|
};
|
|
18718
19082
|
}
|
|
18719
19083
|
if (action === "stdin") {
|
|
19084
|
+
if (typeof record2["data"] === "string" && typeof record2["dataBase64"] === "string") {
|
|
19085
|
+
throw new Error("data \u4E0E dataBase64 \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
19086
|
+
}
|
|
18720
19087
|
const result = await backend.stdin(
|
|
18721
19088
|
target,
|
|
18722
19089
|
jobId,
|
|
@@ -18935,16 +19302,16 @@ function createRemoteFileBackend(deps) {
|
|
|
18935
19302
|
if (carried !== void 0) rememberVersion(target, to, carried);
|
|
18936
19303
|
},
|
|
18937
19304
|
async stat(target, absolute, signal) {
|
|
18938
|
-
const
|
|
18939
|
-
if (
|
|
19305
|
+
const stat7 = await requireBackend(target).stat(absolute, signal);
|
|
19306
|
+
if (stat7.exists) rememberVersion(target, absolute, stat7.version);
|
|
18940
19307
|
return {
|
|
18941
|
-
exists:
|
|
18942
|
-
isDirectory:
|
|
18943
|
-
isFile:
|
|
18944
|
-
isSymlink:
|
|
18945
|
-
size:
|
|
18946
|
-
mtimeMs:
|
|
18947
|
-
version:
|
|
19308
|
+
exists: stat7.exists,
|
|
19309
|
+
isDirectory: stat7.isDirectory,
|
|
19310
|
+
isFile: stat7.isFile,
|
|
19311
|
+
isSymlink: stat7.isSymlink,
|
|
19312
|
+
size: stat7.size,
|
|
19313
|
+
mtimeMs: stat7.mtimeMs,
|
|
19314
|
+
version: stat7.version
|
|
18948
19315
|
};
|
|
18949
19316
|
},
|
|
18950
19317
|
async patch(target, absolute, diff, expectedVersion, signal) {
|
|
@@ -19400,6 +19767,9 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19400
19767
|
const from = resolveOrThrow3(target, rawFrom);
|
|
19401
19768
|
const to = resolveOrThrow3(target, rawTo);
|
|
19402
19769
|
if (from === to) throw new Error(`\u6E90\u4E0E\u76EE\u6807\u662F\u540C\u4E00\u4E2A\u8DEF\u5F84\uFF1A${from}`);
|
|
19770
|
+
if (to.startsWith(from.endsWith("/") ? from : `${from}/`)) {
|
|
19771
|
+
throw new Error(`\u4E0D\u80FD\u628A\u76EE\u5F55\u79FB\u52A8\u5230\u5B83\u81EA\u5DF1\u7684\u5B50\u76EE\u5F55\u91CC\uFF1A${from} \u2192 ${to}`);
|
|
19772
|
+
}
|
|
19403
19773
|
assertWritable(target, to, allowMutation);
|
|
19404
19774
|
const createParents = record2["createParents"] !== false;
|
|
19405
19775
|
await backend.move(target, from, to, createParents, exec.signal);
|
|
@@ -19436,15 +19806,15 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19436
19806
|
const requested = requiredString3(record2["path"], "path");
|
|
19437
19807
|
const target = targetFor3(backend, exec);
|
|
19438
19808
|
const path = resolveOrThrow3(target, requested);
|
|
19439
|
-
const
|
|
19440
|
-
const kind = !
|
|
19809
|
+
const stat7 = await backend.stat(target, path, exec.signal);
|
|
19810
|
+
const kind = !stat7.exists ? "missing" : stat7.isSymlink ? "symlink" : stat7.isDirectory ? "directory" : "file";
|
|
19441
19811
|
return {
|
|
19442
19812
|
path,
|
|
19443
|
-
exists:
|
|
19813
|
+
exists: stat7.exists,
|
|
19444
19814
|
kind,
|
|
19445
|
-
size:
|
|
19446
|
-
mtimeMs:
|
|
19447
|
-
version:
|
|
19815
|
+
size: stat7.exists ? stat7.size : 0,
|
|
19816
|
+
mtimeMs: stat7.exists ? stat7.mtimeMs : 0,
|
|
19817
|
+
version: stat7.exists ? stat7.version : ""
|
|
19448
19818
|
};
|
|
19449
19819
|
}
|
|
19450
19820
|
})
|
|
@@ -19524,11 +19894,11 @@ var LspFrameParser = class {
|
|
|
19524
19894
|
#append(bytes) {
|
|
19525
19895
|
if (bytes.length === 0) return;
|
|
19526
19896
|
if (this.#end + bytes.length > this.#store.length) {
|
|
19527
|
-
const
|
|
19897
|
+
const pending2 = this.#end - this.#start;
|
|
19528
19898
|
if (this.#start > 0) {
|
|
19529
19899
|
this.#store.copyWithin(0, this.#start, this.#end);
|
|
19530
19900
|
this.#start = 0;
|
|
19531
|
-
this.#end =
|
|
19901
|
+
this.#end = pending2;
|
|
19532
19902
|
}
|
|
19533
19903
|
if (this.#end + bytes.length > this.#store.length) {
|
|
19534
19904
|
let capacity = Math.max(this.#store.length, 64 * 1024);
|
|
@@ -19537,7 +19907,7 @@ var LspFrameParser = class {
|
|
|
19537
19907
|
this.#store.copy(next, 0, this.#start, this.#end);
|
|
19538
19908
|
this.#store = next;
|
|
19539
19909
|
this.#start = 0;
|
|
19540
|
-
this.#end =
|
|
19910
|
+
this.#end = pending2;
|
|
19541
19911
|
}
|
|
19542
19912
|
}
|
|
19543
19913
|
this.#store.set(bytes, this.#end);
|
|
@@ -19749,11 +20119,11 @@ var LspSession = class _LspSession {
|
|
|
19749
20119
|
throw error;
|
|
19750
20120
|
}
|
|
19751
20121
|
const onAbort = () => {
|
|
19752
|
-
const
|
|
19753
|
-
if (
|
|
20122
|
+
const pending2 = this.#pending.get(id);
|
|
20123
|
+
if (pending2 === void 0) return;
|
|
19754
20124
|
this.#pending.delete(id);
|
|
19755
|
-
clearTimeout(
|
|
19756
|
-
|
|
20125
|
+
clearTimeout(pending2.timer);
|
|
20126
|
+
pending2.reject(new LspSessionError("\u7B49\u5F85\u8BED\u8A00\u670D\u52A1\u5668\u65F6\u88AB\u53D6\u6D88"));
|
|
19757
20127
|
};
|
|
19758
20128
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
19759
20129
|
try {
|
|
@@ -19877,15 +20247,15 @@ var LspSession = class _LspSession {
|
|
|
19877
20247
|
if (typeof message["method"] !== "string") {
|
|
19878
20248
|
const id = message["id"];
|
|
19879
20249
|
if (typeof id === "number") {
|
|
19880
|
-
const
|
|
19881
|
-
if (
|
|
20250
|
+
const pending2 = this.#pending.get(id);
|
|
20251
|
+
if (pending2 === void 0) return;
|
|
19882
20252
|
this.#pending.delete(id);
|
|
19883
|
-
clearTimeout(
|
|
20253
|
+
clearTimeout(pending2.timer);
|
|
19884
20254
|
const error = message["error"];
|
|
19885
20255
|
if (error !== void 0 && error !== null) {
|
|
19886
|
-
|
|
20256
|
+
pending2.reject(new LspSessionError(`${pending2.method} \u5931\u8D25\uFF1A${describeError(error)}`));
|
|
19887
20257
|
} else {
|
|
19888
|
-
|
|
20258
|
+
pending2.resolve(message["result"]);
|
|
19889
20259
|
}
|
|
19890
20260
|
}
|
|
19891
20261
|
return;
|
|
@@ -19941,10 +20311,10 @@ var LspSession = class _LspSession {
|
|
|
19941
20311
|
}
|
|
19942
20312
|
#failAll(error) {
|
|
19943
20313
|
this.#exitReason ??= error;
|
|
19944
|
-
for (const [id,
|
|
19945
|
-
clearTimeout(
|
|
20314
|
+
for (const [id, pending2] of this.#pending) {
|
|
20315
|
+
clearTimeout(pending2.timer);
|
|
19946
20316
|
this.#pending.delete(id);
|
|
19947
|
-
|
|
20317
|
+
pending2.reject(error);
|
|
19948
20318
|
}
|
|
19949
20319
|
}
|
|
19950
20320
|
};
|
|
@@ -20108,8 +20478,12 @@ function describeInstalled(available) {
|
|
|
20108
20478
|
return `\u8BE5\u4E3B\u673A\u4E0A\u63A2\u6D4B\u5230\u7684\u8BED\u8A00\u670D\u52A1\u5668\uFF1A${list}\u3002`;
|
|
20109
20479
|
}
|
|
20110
20480
|
var MAX_SESSIONS = 8;
|
|
20111
|
-
function
|
|
20112
|
-
return `${target.connectionId}\0${target.root}\0${language}`;
|
|
20481
|
+
function sessionKeyPrefix(target, language) {
|
|
20482
|
+
return `${target.connectionId}\0${target.root}\0${language}\0`;
|
|
20483
|
+
}
|
|
20484
|
+
function sessionKey(target, language, override) {
|
|
20485
|
+
const server = override === void 0 ? "" : `${override.command}\0${(override.args ?? []).join("\0")}`;
|
|
20486
|
+
return `${sessionKeyPrefix(target, language)}${server}`;
|
|
20113
20487
|
}
|
|
20114
20488
|
function createLspRegistry(deps) {
|
|
20115
20489
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -20128,15 +20502,15 @@ function createLspRegistry(deps) {
|
|
|
20128
20502
|
}
|
|
20129
20503
|
return {
|
|
20130
20504
|
async session(target, language, override, signal) {
|
|
20131
|
-
const key = sessionKey(target, language);
|
|
20505
|
+
const key = sessionKey(target, language, override);
|
|
20132
20506
|
const existing = sessions.get(key);
|
|
20133
20507
|
if (existing !== void 0 && !existing.session.stopped) {
|
|
20134
20508
|
sessions.delete(key);
|
|
20135
20509
|
sessions.set(key, existing);
|
|
20136
20510
|
return existing.session;
|
|
20137
20511
|
}
|
|
20138
|
-
const
|
|
20139
|
-
if (
|
|
20512
|
+
const pending2 = inFlight.get(key);
|
|
20513
|
+
if (pending2 !== void 0) return pending2;
|
|
20140
20514
|
const attempt = (async () => {
|
|
20141
20515
|
const command = override ?? defaultServerFor(language);
|
|
20142
20516
|
if (command === void 0) {
|
|
@@ -20208,17 +20582,21 @@ ${describeInstalled(available)}`);
|
|
|
20208
20582
|
}));
|
|
20209
20583
|
},
|
|
20210
20584
|
async stop(target, language, signal) {
|
|
20211
|
-
const
|
|
20212
|
-
const
|
|
20213
|
-
if (
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20221
|
-
|
|
20585
|
+
const prefix = sessionKeyPrefix(target, language);
|
|
20586
|
+
const keys = [...new Set([...sessions.keys(), ...inFlight.keys()].filter((key) => key.startsWith(prefix)))];
|
|
20587
|
+
if (keys.length === 0) return false;
|
|
20588
|
+
for (const key of keys) {
|
|
20589
|
+
const starting = inFlight.get(key);
|
|
20590
|
+
if (starting !== void 0) {
|
|
20591
|
+
retired.add(key);
|
|
20592
|
+
await starting.catch(() => void 0);
|
|
20593
|
+
retired.delete(key);
|
|
20594
|
+
}
|
|
20595
|
+
const entry = sessions.get(key);
|
|
20596
|
+
if (entry === void 0) continue;
|
|
20597
|
+
sessions.delete(key);
|
|
20598
|
+
await entry.session.close(signal);
|
|
20599
|
+
}
|
|
20222
20600
|
return true;
|
|
20223
20601
|
},
|
|
20224
20602
|
async disposeAll() {
|
|
@@ -21020,7 +21398,6 @@ function registerRemoteLspTools(ctx, deps) {
|
|
|
21020
21398
|
// src/wiring/tools-session.ts
|
|
21021
21399
|
import { cp, mkdir as mkdir10, readFile as readFile9, readdir as readdir6, rename as rename6, rm as rm5, stat as nodeStat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
21022
21400
|
import { dirname as dirname8, join as join17 } from "node:path";
|
|
21023
|
-
import { homedir as homedir6 } from "node:os";
|
|
21024
21401
|
|
|
21025
21402
|
// src/host/integration/session/session-labels.ts
|
|
21026
21403
|
function normalizePath(path) {
|
|
@@ -21070,8 +21447,8 @@ function createWorkspaceTitleResolver(ctx) {
|
|
|
21070
21447
|
// src/wiring/tools-session.ts
|
|
21071
21448
|
function wireSessionAdmin(env) {
|
|
21072
21449
|
const { ctx, limits, notifySessionRemoved: notifySessionRemoved2 } = env;
|
|
21073
|
-
const sessionsRoot = limits.sessionsRoot ?? join17(
|
|
21074
|
-
const storagesRoot = join17(
|
|
21450
|
+
const sessionsRoot = limits.sessionsRoot ?? join17(dshHome(), "sessions");
|
|
21451
|
+
const storagesRoot = join17(dshHome(), "storages");
|
|
21075
21452
|
const sessionBinRoot = limits.sessionBinRoot ?? join17(storagesRoot, "over-ssh", "session-trash");
|
|
21076
21453
|
if (limits.sessionBinRoot === void 0) {
|
|
21077
21454
|
const carried = carryDirectory(join17(storagesRoot, "remote-workspace", "session-trash"), sessionBinRoot);
|
|
@@ -21122,15 +21499,19 @@ function wireSessionAdmin(env) {
|
|
|
21122
21499
|
throw error;
|
|
21123
21500
|
}
|
|
21124
21501
|
},
|
|
21125
|
-
|
|
21502
|
+
// The listing is already in hand: hand it over so the header cache can key on it instead of re-listing.
|
|
21503
|
+
readHeader: (dir, listing) => readSessionHeader(
|
|
21504
|
+
dir,
|
|
21505
|
+
listing?.map((entry) => ({ name: entry.name, size: entry.bytes, mtimeMs: entry.mtimeMs }))
|
|
21506
|
+
),
|
|
21126
21507
|
// Both resolvers are optional: a profile without the platform services still lists sessions, labelled
|
|
21127
21508
|
// by their directory and id rather than by name.
|
|
21128
21509
|
resolveSessionTitle: createSessionTitleResolver(ctx),
|
|
21129
21510
|
resolveWorkspaceTitle: createWorkspaceTitleResolver(ctx),
|
|
21130
21511
|
stat: async (path) => {
|
|
21131
21512
|
try {
|
|
21132
|
-
const
|
|
21133
|
-
return { isDirectory:
|
|
21513
|
+
const stat7 = await nodeStat2(path);
|
|
21514
|
+
return { isDirectory: stat7.isDirectory(), bytes: stat7.size };
|
|
21134
21515
|
} catch {
|
|
21135
21516
|
return void 0;
|
|
21136
21517
|
}
|
|
@@ -21371,6 +21752,7 @@ function managementToolDefinitions(backend) {
|
|
|
21371
21752
|
throw new Error("expectedVersion / expectedSha256 / force \u53EA\u5BF9 upload \u6709\u610F\u4E49\uFF1B\u4E0B\u8F7D\u7531\u672C\u5730\u6587\u4EF6\u7CFB\u7EDF\u8986\u76D6\uFF0C\u4E0D\u505A\u8FDC\u7AEF\u5B88\u536B");
|
|
21372
21753
|
}
|
|
21373
21754
|
const target = targetFor4(backend, exec);
|
|
21755
|
+
if (direction === "upload") backend.assertWritable?.(target, "\u4E0A\u4F20\u8FDC\u7AEF\u6587\u4EF6");
|
|
21374
21756
|
if (direction === "upload" && backend.statRemote !== void 0 && (expectedVersion !== void 0 || !force && expectedSha256 === void 0)) {
|
|
21375
21757
|
const absolute = resolveUnderRoot(target.root, remotePath);
|
|
21376
21758
|
if (typeof absolute !== "string") throw new Error(absolute.error);
|
|
@@ -21458,6 +21840,7 @@ function managementToolDefinitions(backend) {
|
|
|
21458
21840
|
const target = targetFor4(backend, exec);
|
|
21459
21841
|
const resolved = resolveUnderRoot(target.root, requested, void 0, target.connectionId);
|
|
21460
21842
|
if (typeof resolved !== "string") throw new Error(resolved.error);
|
|
21843
|
+
backend.assertWritable?.(target, mode === "permanent" ? "\u6C38\u4E45\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6" : "\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6");
|
|
21461
21844
|
const result = await backend.discard({ target, path: resolved, mode, signal: exec.signal });
|
|
21462
21845
|
return {
|
|
21463
21846
|
mode: result.mode,
|
|
@@ -21517,6 +21900,8 @@ function managementToolDefinitions(backend) {
|
|
|
21517
21900
|
throw new Error("id \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u7701\u7565\u5B83\u8868\u793A\u64A4\u9500\u6700\u8FD1\u4E00\u6B21\u5220\u9664\uFF09");
|
|
21518
21901
|
}
|
|
21519
21902
|
const target = targetFor4(backend, exec);
|
|
21903
|
+
const listing = record2["list"] === true;
|
|
21904
|
+
if (!listing) backend.assertWritable?.(target, "\u4ECE\u56DE\u6536\u7AD9\u6062\u590D\u8FDC\u7AEF\u6587\u4EF6");
|
|
21520
21905
|
const outcome = await backend.restore({
|
|
21521
21906
|
target,
|
|
21522
21907
|
id: typeof id === "string" ? id : void 0,
|
|
@@ -21573,6 +21958,7 @@ function managementToolDefinitions(backend) {
|
|
|
21573
21958
|
if (typeof source !== "string") throw new Error(source.error);
|
|
21574
21959
|
const destination = resolveUnderRoot(target.root, to, void 0, target.connectionId);
|
|
21575
21960
|
if (typeof destination !== "string") throw new Error(destination.error);
|
|
21961
|
+
backend.assertWritable?.(target, "\u590D\u5236\u8FDC\u7AEF\u6587\u4EF6");
|
|
21576
21962
|
const outcome = await backend.copy({
|
|
21577
21963
|
target,
|
|
21578
21964
|
from: source,
|
|
@@ -21807,6 +22193,8 @@ function wireManagementTools(env) {
|
|
|
21807
22193
|
)?.root;
|
|
21808
22194
|
const managementBackend = {
|
|
21809
22195
|
resolve: (cwd) => toolBackend.resolve(cwd),
|
|
22196
|
+
// 管理工具(上传/删除/恢复/复制)之前完全不过写闸:read-only 会话照样能删文件(审计 2026-11-06 宿主 P0)。
|
|
22197
|
+
assertWritable: (target, action) => env.assertWritable(target, action),
|
|
21810
22198
|
// ADR-0013: the session id (plus its fork lineage) is the strict scope; `cwd` alone cannot separate
|
|
21811
22199
|
// two conversations opened on the same directory.
|
|
21812
22200
|
resolveSessionRoot: (cwd, sessionId) => sessionRoots.outcomeFor(cwd, sessionIdentityOf(ctx, sessionId)),
|
|
@@ -21926,6 +22314,7 @@ function wireManagementTools(env) {
|
|
|
21926
22314
|
}
|
|
21927
22315
|
|
|
21928
22316
|
// src/wiring/tools.ts
|
|
22317
|
+
init_errors();
|
|
21929
22318
|
function sessionRootsFilePath() {
|
|
21930
22319
|
return join18(resolveStateDir(), USER_ROOTS_FILE_NAME);
|
|
21931
22320
|
}
|
|
@@ -21966,7 +22355,8 @@ function wireTools(env) {
|
|
|
21966
22355
|
disposeJobTools,
|
|
21967
22356
|
disposeFileTools,
|
|
21968
22357
|
lspRegistry,
|
|
21969
|
-
disposeLspTools
|
|
22358
|
+
disposeLspTools,
|
|
22359
|
+
assertWritable: assertWritable2
|
|
21970
22360
|
} = wireFileAndJobTools({ ctx, limits, profiles, transports, registry, sessionRoots });
|
|
21971
22361
|
const sessionAdmin = wireSessionAdmin({ ctx, limits, notifySessionRemoved: notifySessionRemoved2 });
|
|
21972
22362
|
const { dispose: disposeManagement } = wireManagementTools({
|
|
@@ -21980,7 +22370,8 @@ function wireTools(env) {
|
|
|
21980
22370
|
sessionRoots,
|
|
21981
22371
|
recordFor,
|
|
21982
22372
|
transferService,
|
|
21983
|
-
transferTracker
|
|
22373
|
+
transferTracker,
|
|
22374
|
+
assertWritable: assertWritable2
|
|
21984
22375
|
});
|
|
21985
22376
|
const disposeStrictArguments = strictArgumentsGate(ctx);
|
|
21986
22377
|
return {
|
|
@@ -22049,7 +22440,9 @@ function wireAgentTools(env) {
|
|
|
22049
22440
|
disposeRemoteTools();
|
|
22050
22441
|
disposeGitTool();
|
|
22051
22442
|
disposeForwardTool();
|
|
22052
|
-
void forwardBackend.closeAll()
|
|
22443
|
+
void forwardBackend.closeAll().catch((error) => {
|
|
22444
|
+
ctx.logger?.warn?.(`over-ssh: \u5173\u95ED\u7AEF\u53E3\u8F6C\u53D1\u65F6\u51FA\u9519\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
22445
|
+
});
|
|
22053
22446
|
};
|
|
22054
22447
|
return { sessionRoots, toolBackend, disposeTools };
|
|
22055
22448
|
}
|
|
@@ -22086,21 +22479,33 @@ function wireFileAndJobTools(env) {
|
|
|
22086
22479
|
backendFor: (target) => recordFor(target)?.backend,
|
|
22087
22480
|
...limits.maxReadBytes === void 0 ? {} : { maxReadBytes: limits.maxReadBytes }
|
|
22088
22481
|
});
|
|
22089
|
-
const allowMutation = (target) => {
|
|
22482
|
+
const allowMutation = (target, action) => {
|
|
22483
|
+
const policy = platformMutationPolicy(ctx);
|
|
22484
|
+
const sessionId = currentSessionId();
|
|
22485
|
+
const cwd = sessionId === void 0 || sessionId === "" ? void 0 : readSessionCwd(ctx, sessionId);
|
|
22090
22486
|
const refusal = mutationRefusal(
|
|
22091
|
-
`${target.connectionId}:${target.root}`,
|
|
22487
|
+
action === void 0 ? `${target.connectionId}:${target.root}` : `${action}\uFF1A${target.connectionId}:${target.root}`,
|
|
22092
22488
|
{
|
|
22093
|
-
...
|
|
22489
|
+
...policy,
|
|
22094
22490
|
rootArea: () => {
|
|
22095
22491
|
const record2 = recordFor(target);
|
|
22096
22492
|
if (record2 === void 0) return "deny";
|
|
22097
|
-
return workspaceWriteArea(record2.root,
|
|
22493
|
+
return workspaceWriteArea(record2.root, policy.policy?.workspaceRoot, cwd, sessionIdentityOf(ctx, sessionId));
|
|
22098
22494
|
}
|
|
22099
22495
|
}
|
|
22100
22496
|
);
|
|
22101
22497
|
if (refusal !== void 0) throw new Error(refusal);
|
|
22102
22498
|
};
|
|
22103
22499
|
const disposeFileTools = registerRemoteFileTools(ctx, fileBackend, { allowMutation });
|
|
22500
|
+
const assertWritable2 = (target, action) => {
|
|
22501
|
+
allowMutation(target, action);
|
|
22502
|
+
if (recordFor(target)?.root.writable === false) {
|
|
22503
|
+
throw new SshDevError("SANDBOX_DENIED", `\u8FD9\u4E2A\u6839\u5728\u914D\u7F6E\u91CC\u662F\u53EA\u8BFB\u7684\uFF08writable: false\uFF09\uFF0C\u62D2\u7EDD\u5199\u5165\uFF1A${action}`, {
|
|
22504
|
+
connectionId: target.connectionId,
|
|
22505
|
+
root: target.root
|
|
22506
|
+
});
|
|
22507
|
+
}
|
|
22508
|
+
};
|
|
22104
22509
|
const lspRegistry = createLspRegistry({
|
|
22105
22510
|
acquire: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport,
|
|
22106
22511
|
onLog: (connectionId, message) => {
|
|
@@ -22108,7 +22513,7 @@ function wireFileAndJobTools(env) {
|
|
|
22108
22513
|
}
|
|
22109
22514
|
});
|
|
22110
22515
|
const disposeLspTools = registerRemoteLspTools(ctx, { backend: fileBackend, registry: lspRegistry });
|
|
22111
|
-
return { recordFor, disposeJobTools, disposeFileTools, lspRegistry, disposeLspTools };
|
|
22516
|
+
return { recordFor, disposeJobTools, disposeFileTools, lspRegistry, disposeLspTools, assertWritable: assertWritable2 };
|
|
22112
22517
|
}
|
|
22113
22518
|
|
|
22114
22519
|
// src/index.ts
|