dsh-over-ssh 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -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/cordis.patch.yml +10 -12
- package/lib/client.js +194 -62
- package/lib/file-references.js +153 -13
- package/lib/fs.js +87 -18
- package/lib/index.js +710 -179
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +28 -2
- package/lib/types/src/host/domain/session/session-admin.d.ts +8 -0
- 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/local-bridge.d.ts +4 -4
- package/lib/types/src/wiring/helpers.d.ts +1 -1
- package/package.json +1 -5
- package/lib/local-bridge-refs.js +0 -16
- package/lib/types/src/local-bridge-refs.d.ts +0 -25
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
|
}
|
|
@@ -4321,6 +4372,7 @@ function parseSessionTrashIndex(text8) {
|
|
|
4321
4372
|
segment: record2["segment"],
|
|
4322
4373
|
originalSegment: record2["originalSegment"],
|
|
4323
4374
|
title: typeof record2["title"] === "string" ? record2["title"] : void 0,
|
|
4375
|
+
...typeof record2["parentSessionId"] === "string" && record2["parentSessionId"] !== "" ? { parentSessionId: record2["parentSessionId"] } : {},
|
|
4324
4376
|
bytes: typeof record2["bytes"] === "number" && Number.isFinite(record2["bytes"]) ? record2["bytes"] : 0,
|
|
4325
4377
|
deletedAtMs: record2["deletedAtMs"],
|
|
4326
4378
|
movedBy: record2["movedBy"] === "copy" ? "copy" : "rename",
|
|
@@ -4367,6 +4419,7 @@ function makeSessionTrashEntry(input) {
|
|
|
4367
4419
|
segment: input.segment,
|
|
4368
4420
|
originalSegment: input.originalSegment,
|
|
4369
4421
|
title: input.title,
|
|
4422
|
+
...input.parentSessionId === void 0 ? {} : { parentSessionId: input.parentSessionId },
|
|
4370
4423
|
bytes: input.bytes,
|
|
4371
4424
|
deletedAtMs: input.deletedAtMs,
|
|
4372
4425
|
movedBy: input.movedBy,
|
|
@@ -4494,13 +4547,14 @@ function createSessionAdminContext(deps) {
|
|
|
4494
4547
|
const dir = joinPath(joinPath(sessionsRoot, project), session.name);
|
|
4495
4548
|
const contents = await deps.listDir(dir);
|
|
4496
4549
|
let bytes = 0;
|
|
4497
|
-
let updatedAtMs = session.mtimeMs;
|
|
4550
|
+
let updatedAtMs = Math.trunc(session.mtimeMs);
|
|
4498
4551
|
for (const item of contents) {
|
|
4499
4552
|
if (item.isDirectory) continue;
|
|
4500
4553
|
bytes += item.bytes;
|
|
4501
|
-
|
|
4554
|
+
const itemMs = Math.trunc(item.mtimeMs);
|
|
4555
|
+
if (itemMs > updatedAtMs) updatedAtMs = itemMs;
|
|
4502
4556
|
}
|
|
4503
|
-
const header = await deps.readHeader(dir);
|
|
4557
|
+
const header = await deps.readHeader(dir, contents);
|
|
4504
4558
|
if (header === void 0) {
|
|
4505
4559
|
return {
|
|
4506
4560
|
id: session.name,
|
|
@@ -4641,6 +4695,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4641
4695
|
segment: entryId,
|
|
4642
4696
|
originalSegment: record2.segment,
|
|
4643
4697
|
title: record2.title,
|
|
4698
|
+
parentSessionId: record2.parentSessionId,
|
|
4644
4699
|
bytes: record2.bytes,
|
|
4645
4700
|
deletedAtMs,
|
|
4646
4701
|
movedBy,
|
|
@@ -4758,31 +4813,63 @@ function createSessionAdminService(deps) {
|
|
|
4758
4813
|
if (entry === void 0) {
|
|
4759
4814
|
throw new SshDevError("TRASH_NOT_FOUND", `\u56DE\u6536\u7AD9\u6761\u76EE\u4E0D\u5B58\u5728\uFF1A${entryId}`, { entryId });
|
|
4760
4815
|
}
|
|
4761
|
-
const
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4816
|
+
const restoreOne = async (item) => {
|
|
4817
|
+
const target = joinPath(sessionsRoot, item.projectDir, item.originalSegment);
|
|
4818
|
+
const occupied = await deps.stat(target) !== void 0;
|
|
4819
|
+
const plan = planRestoreSession(item, { occupied });
|
|
4820
|
+
if (plan.kind === "refuse") {
|
|
4821
|
+
throw new SshDevError("TRANSFER_CONFLICT", `\u539F\u8DEF\u5F84\u5DF2\u88AB\u5360\u7528\uFF0C\u62D2\u7EDD\u9759\u9ED8\u6362\u540D\u8FD8\u539F\uFF1A${target}`, {
|
|
4822
|
+
entryId: item.id,
|
|
4823
|
+
suggestedId: plan.suggestedId,
|
|
4824
|
+
target
|
|
4825
|
+
});
|
|
4826
|
+
}
|
|
4827
|
+
const from = entryDir(item.segment);
|
|
4828
|
+
let movedBy;
|
|
4829
|
+
try {
|
|
4830
|
+
movedBy = await deps.moveDir(from, target);
|
|
4831
|
+
} catch (error) {
|
|
4832
|
+
const landed = await deps.stat(target) !== void 0;
|
|
4833
|
+
if (!landed) {
|
|
4834
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u8FD8\u539F\u4F1A\u8BDD\u5931\u8D25\uFF1A${item.sessionId}`, {
|
|
4835
|
+
entryId: item.id,
|
|
4836
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
movedBy = "copy";
|
|
4840
|
+
}
|
|
4841
|
+
return { entry: item, restoredPath: target, movedBy };
|
|
4842
|
+
};
|
|
4843
|
+
const parentStillBinned = entry.parentSessionId !== void 0 && entries.some((item) => item.sessionId === entry.parentSessionId);
|
|
4844
|
+
const restored = await restoreOne(entry);
|
|
4845
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4846
|
+
for (const item of entries) {
|
|
4847
|
+
if (item.id === entry.id || item.parentSessionId === void 0) continue;
|
|
4848
|
+
const bucket = childrenOf.get(item.parentSessionId);
|
|
4849
|
+
if (bucket === void 0) childrenOf.set(item.parentSessionId, [item]);
|
|
4850
|
+
else bucket.push(item);
|
|
4851
|
+
}
|
|
4852
|
+
const restoredSubsessions = [];
|
|
4853
|
+
const failedSubsessions = [];
|
|
4854
|
+
const removedIds = /* @__PURE__ */ new Set([entry.id]);
|
|
4855
|
+
const queue = [...childrenOf.get(entry.sessionId) ?? []];
|
|
4856
|
+
while (queue.length > 0) {
|
|
4857
|
+
const child = queue.shift();
|
|
4858
|
+
if (child === void 0) break;
|
|
4859
|
+
if (removedIds.has(child.id)) continue;
|
|
4860
|
+
try {
|
|
4861
|
+
const childRestored = await restoreOne(child);
|
|
4862
|
+
restoredSubsessions.push({ sessionId: child.sessionId, restoredPath: childRestored.restoredPath });
|
|
4863
|
+
removedIds.add(child.id);
|
|
4864
|
+
queue.push(...childrenOf.get(child.sessionId) ?? []);
|
|
4865
|
+
} catch (error) {
|
|
4866
|
+
failedSubsessions.push({
|
|
4867
|
+
sessionId: child.sessionId,
|
|
4868
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
4781
4869
|
});
|
|
4782
4870
|
}
|
|
4783
|
-
movedBy = "copy";
|
|
4784
4871
|
}
|
|
4785
|
-
const remaining = entries.filter((item) => item.id
|
|
4872
|
+
const remaining = entries.filter((item) => !removedIds.has(item.id));
|
|
4786
4873
|
try {
|
|
4787
4874
|
await writeIndex(remaining);
|
|
4788
4875
|
} catch (error) {
|
|
@@ -4791,7 +4878,7 @@ function createSessionAdminService(deps) {
|
|
|
4791
4878
|
cause: error instanceof Error ? error.message : String(error)
|
|
4792
4879
|
});
|
|
4793
4880
|
}
|
|
4794
|
-
return {
|
|
4881
|
+
return { ...restored, restoredSubsessions, failedSubsessions, parentStillBinned };
|
|
4795
4882
|
});
|
|
4796
4883
|
const purge = (entryId, options = {}) => {
|
|
4797
4884
|
void options;
|
|
@@ -4803,7 +4890,6 @@ function createSessionAdminService(deps) {
|
|
|
4803
4890
|
const ordered = [...entries].sort((left, right) => left.deletedAtMs - right.deletedAtMs);
|
|
4804
4891
|
const doomed = [];
|
|
4805
4892
|
const nowMs = deps.now();
|
|
4806
|
-
let keptBytes = ordered.reduce((sum, item) => sum + item.bytes, 0);
|
|
4807
4893
|
if (policy.maxAgeMs > 0) {
|
|
4808
4894
|
for (const entry of ordered) {
|
|
4809
4895
|
if (nowMs - entry.deletedAtMs > policy.maxAgeMs) doomed.push(entry);
|
|
@@ -4812,6 +4898,7 @@ function createSessionAdminService(deps) {
|
|
|
4812
4898
|
const doomedIds = new Set(doomed.map((entry) => entry.id));
|
|
4813
4899
|
const survivors = ordered.filter((entry) => !doomedIds.has(entry.id));
|
|
4814
4900
|
if (policy.maxBytes > 0) {
|
|
4901
|
+
let keptBytes = survivors.reduce((sum, item) => sum + item.bytes, 0);
|
|
4815
4902
|
for (const entry of survivors) {
|
|
4816
4903
|
if (keptBytes <= policy.maxBytes) break;
|
|
4817
4904
|
doomed.push(entry);
|
|
@@ -4824,7 +4911,7 @@ function createSessionAdminService(deps) {
|
|
|
4824
4911
|
const excess = stillKept.length - policy.maxEntries;
|
|
4825
4912
|
for (const entry of stillKept.slice(0, excess)) {
|
|
4826
4913
|
doomed.push(entry);
|
|
4827
|
-
|
|
4914
|
+
doomedIds.add(entry.id);
|
|
4828
4915
|
}
|
|
4829
4916
|
}
|
|
4830
4917
|
const outcome = await purgeEntries(doomed, entries);
|
|
@@ -4840,11 +4927,54 @@ function createSessionAdminService(deps) {
|
|
|
4840
4927
|
}
|
|
4841
4928
|
|
|
4842
4929
|
// src/host/integration/session/session-header.ts
|
|
4843
|
-
import { open, readdir } from "node:fs/promises";
|
|
4930
|
+
import { open, readdir, stat } from "node:fs/promises";
|
|
4844
4931
|
import { join } from "node:path";
|
|
4845
4932
|
import { createZstdDecompress } from "node:zlib";
|
|
4846
4933
|
var SESSION_HEADER_READ_BYTES = 64 * 1024;
|
|
4847
4934
|
var SESSION_HEADER_RETRY_BYTES = 1024 * 1024;
|
|
4935
|
+
function createCachedSessionHeaderReader(io, options = {}) {
|
|
4936
|
+
const inner = createSessionHeaderReader(io);
|
|
4937
|
+
const stat7 = io.stat;
|
|
4938
|
+
if (stat7 === void 0) return inner;
|
|
4939
|
+
const maxEntries = Math.max(1, Math.floor(options.maxEntries ?? SESSION_HEADER_CACHE_MAX));
|
|
4940
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4941
|
+
return async (dir, listing) => {
|
|
4942
|
+
let signature;
|
|
4943
|
+
if (listing !== void 0) {
|
|
4944
|
+
const log = listing.find((entry) => entry.name.startsWith("session.v") && entry.name.endsWith(".zstd"));
|
|
4945
|
+
if (log !== void 0) signature = `${log.size}:${Math.trunc(log.mtimeMs)}`;
|
|
4946
|
+
} else {
|
|
4947
|
+
try {
|
|
4948
|
+
const names = await io.listDir(dir);
|
|
4949
|
+
const log = names.find((name2) => name2.startsWith("session.v") && name2.endsWith(".zstd"));
|
|
4950
|
+
if (log !== void 0) {
|
|
4951
|
+
const info = await stat7(join(dir, log));
|
|
4952
|
+
signature = `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
4953
|
+
}
|
|
4954
|
+
} catch {
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
if (signature !== void 0) {
|
|
4958
|
+
const hit = cache.get(dir);
|
|
4959
|
+
if (hit !== void 0 && hit.signature === signature) {
|
|
4960
|
+
cache.delete(dir);
|
|
4961
|
+
cache.set(dir, hit);
|
|
4962
|
+
return hit.header;
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
const header = await inner(dir);
|
|
4966
|
+
if (signature !== void 0) {
|
|
4967
|
+
cache.set(dir, { signature, header });
|
|
4968
|
+
while (cache.size > maxEntries) {
|
|
4969
|
+
const oldest = cache.keys().next();
|
|
4970
|
+
if (oldest.done === true) break;
|
|
4971
|
+
cache.delete(oldest.value);
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
return header;
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4977
|
+
var SESSION_HEADER_CACHE_MAX = 2048;
|
|
4848
4978
|
function parseSessionHeaderLine(text8) {
|
|
4849
4979
|
const firstLine = text8.split("\n")[0];
|
|
4850
4980
|
if (firstLine === void 0 || firstLine === "") return void 0;
|
|
@@ -4930,11 +5060,16 @@ function createDiskSessionHeaderIo() {
|
|
|
4930
5060
|
return {
|
|
4931
5061
|
listDir: (dir) => readdir(dir),
|
|
4932
5062
|
readHead: readHeadFromDisk,
|
|
4933
|
-
decompress: decompressPrefix
|
|
5063
|
+
decompress: decompressPrefix,
|
|
5064
|
+
// The cache's only input: `(size, mtime)` is what a session log's append changes.
|
|
5065
|
+
stat: async (path) => {
|
|
5066
|
+
const info = await stat(path);
|
|
5067
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
5068
|
+
}
|
|
4934
5069
|
};
|
|
4935
5070
|
}
|
|
4936
5071
|
function createDiskSessionHeaderReader() {
|
|
4937
|
-
return
|
|
5072
|
+
return createCachedSessionHeaderReader(createDiskSessionHeaderIo());
|
|
4938
5073
|
}
|
|
4939
5074
|
|
|
4940
5075
|
// src/host/app/routed-fs.ts
|
|
@@ -5820,17 +5955,25 @@ var RootRegistry = class {
|
|
|
5820
5955
|
* sessions at all: an empty list means "the session service was not readable", not "no session is live", and
|
|
5821
5956
|
* dropping every grant on a transient read failure would break live turns.
|
|
5822
5957
|
*
|
|
5823
|
-
* @param cwds - working directories of the live sessions.
|
|
5958
|
+
* @param cwds - working directories of the live sessions, optionally with the session id that owns each one.
|
|
5959
|
+
* With an id the root carries `sessions: [id]` and is writable **only by that session**; without one it is a
|
|
5960
|
+
* workspace-scoped root (the legacy shape tests and agentless compositions use).
|
|
5824
5961
|
* @returns the session roots this call added (empty when every cwd was already covered).
|
|
5825
5962
|
*/
|
|
5826
5963
|
syncSessionWorkspaces(cwds) {
|
|
5827
5964
|
if (cwds.length === 0) return [];
|
|
5828
5965
|
const live = /* @__PURE__ */ new Set();
|
|
5829
5966
|
const added = [];
|
|
5830
|
-
for (const
|
|
5967
|
+
for (const item of cwds) {
|
|
5968
|
+
const cwd = typeof item === "string" ? item : item.cwd;
|
|
5969
|
+
const sessionId = typeof item === "string" ? void 0 : item.sessionId;
|
|
5831
5970
|
if (typeof cwd !== "string" || cwd === "") continue;
|
|
5832
5971
|
if (!isAbsoluteLocalPath(normalizeLocalPath2(cwd, { caseInsensitive: false }))) continue;
|
|
5833
|
-
const root = makeLocalRoot(cwd, {
|
|
5972
|
+
const root = makeLocalRoot(cwd, {
|
|
5973
|
+
workspaces: [cwd],
|
|
5974
|
+
...sessionId === void 0 || sessionId === "" ? {} : { sessions: [sessionId] },
|
|
5975
|
+
writable: true
|
|
5976
|
+
});
|
|
5834
5977
|
const key = rootKey(root);
|
|
5835
5978
|
live.add(key);
|
|
5836
5979
|
if (this.#localRootCovers(cwd)) continue;
|
|
@@ -6007,7 +6150,7 @@ function carryMissingState(legacy, current, files, dirs) {
|
|
|
6007
6150
|
renameSync(temp, to);
|
|
6008
6151
|
} catch (error) {
|
|
6009
6152
|
rmSync(temp, { force: true });
|
|
6010
|
-
failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6153
|
+
if (!existsSync(to)) failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6011
6154
|
}
|
|
6012
6155
|
}
|
|
6013
6156
|
for (const name2 of dirs) {
|
|
@@ -6020,7 +6163,7 @@ function carryMissingState(legacy, current, files, dirs) {
|
|
|
6020
6163
|
renameSync(temp, to);
|
|
6021
6164
|
} catch (error) {
|
|
6022
6165
|
rmSync(temp, { recursive: true, force: true });
|
|
6023
|
-
failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6166
|
+
if (!existsSync(to)) failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6024
6167
|
}
|
|
6025
6168
|
}
|
|
6026
6169
|
return failures;
|
|
@@ -6446,9 +6589,9 @@ var AgentFrameParser = class {
|
|
|
6446
6589
|
*/
|
|
6447
6590
|
#append(chunk) {
|
|
6448
6591
|
if (chunk.byteLength === 0) return;
|
|
6449
|
-
const
|
|
6450
|
-
if (this.#store.byteLength - this.#start <
|
|
6451
|
-
const needed =
|
|
6592
|
+
const pending2 = this.#end - this.#start;
|
|
6593
|
+
if (this.#store.byteLength - this.#start < pending2 + chunk.byteLength) {
|
|
6594
|
+
const needed = pending2 + chunk.byteLength;
|
|
6452
6595
|
if (this.#store.byteLength >= needed) {
|
|
6453
6596
|
this.#store.copyWithin(0, this.#start, this.#end);
|
|
6454
6597
|
} else {
|
|
@@ -6457,7 +6600,7 @@ var AgentFrameParser = class {
|
|
|
6457
6600
|
this.#store = grown;
|
|
6458
6601
|
}
|
|
6459
6602
|
this.#start = 0;
|
|
6460
|
-
this.#end =
|
|
6603
|
+
this.#end = pending2;
|
|
6461
6604
|
}
|
|
6462
6605
|
this.#store.set(chunk, this.#end);
|
|
6463
6606
|
this.#end += chunk.byteLength;
|
|
@@ -7242,9 +7385,9 @@ var AgentProtocolClient = class {
|
|
|
7242
7385
|
failConnection(error) {
|
|
7243
7386
|
if (this.#lost !== void 0) return;
|
|
7244
7387
|
this.#lost = error;
|
|
7245
|
-
const
|
|
7388
|
+
const pending2 = [...this.#pending.values()];
|
|
7246
7389
|
this.#pending.clear();
|
|
7247
|
-
for (const entry of
|
|
7390
|
+
for (const entry of pending2) entry.settle({ error });
|
|
7248
7391
|
for (const channel of this.#blobQueue) channel.fail(error);
|
|
7249
7392
|
this.#blobQueue.length = 0;
|
|
7250
7393
|
this.#blobById.clear();
|
|
@@ -8129,14 +8272,14 @@ var AgentTransport = class extends TransportBase {
|
|
|
8129
8272
|
}
|
|
8130
8273
|
}
|
|
8131
8274
|
if (expectedVersion === void 0) {
|
|
8132
|
-
let
|
|
8275
|
+
let stat7;
|
|
8133
8276
|
try {
|
|
8134
|
-
|
|
8277
|
+
stat7 = asRecord(await client.request("fs.stat", { path: request.path }, this.#requestOptions(void 0, signal)));
|
|
8135
8278
|
} catch (error) {
|
|
8136
8279
|
throw this.#translate(error);
|
|
8137
8280
|
}
|
|
8138
|
-
const exists =
|
|
8139
|
-
const observed = typeof
|
|
8281
|
+
const exists = stat7?.["exists"] === true;
|
|
8282
|
+
const observed = typeof stat7?.["version"] === "string" ? stat7["version"] : void 0;
|
|
8140
8283
|
if (!exists || observed === void 0 || observed !== expected) {
|
|
8141
8284
|
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
8285
|
connectionId: this.id,
|
|
@@ -8645,9 +8788,16 @@ var AgentTransport = class extends TransportBase {
|
|
|
8645
8788
|
state: typeof record2["state"] === "string" ? record2["state"] : "unknown"
|
|
8646
8789
|
};
|
|
8647
8790
|
}
|
|
8648
|
-
/**
|
|
8649
|
-
|
|
8650
|
-
|
|
8791
|
+
/**
|
|
8792
|
+
* Read a language server's output from `offset`: `lsp.read`. Same offset semantics as {@link procOutput}.
|
|
8793
|
+
*
|
|
8794
|
+
* `maxBytes` bounds one response. The agent defaults it to its 2 MiB chunk; callers that poll (the LSP pump)
|
|
8795
|
+
* pass a smaller cap so a quiet server does not cost 2 MiB of transient host memory per poll. Agents older
|
|
8796
|
+
* than the field ignore it (Go's JSON decoder ignores unknown fields), so it is safe to always send.
|
|
8797
|
+
*/
|
|
8798
|
+
async lspRead(id, offset = 0, signal, maxBytes) {
|
|
8799
|
+
const params = maxBytes === void 0 ? { id, offset } : { id, offset, maxBytes: Math.max(1, Math.floor(maxBytes)) };
|
|
8800
|
+
const result = await this.#rpc("lsp.read", params, signal);
|
|
8651
8801
|
return readLspRead(result);
|
|
8652
8802
|
}
|
|
8653
8803
|
/** One language-server session's state: `lsp.status`. */
|
|
@@ -9394,7 +9544,7 @@ function localVersion(info) {
|
|
|
9394
9544
|
init_temp_name();
|
|
9395
9545
|
init_errors();
|
|
9396
9546
|
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";
|
|
9547
|
+
import { access, chmod, lstat, mkdir, open as open2, readdir as readdir2, rename, rm, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
9398
9548
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
9399
9549
|
var LIST_STAT_CONCURRENCY = 32;
|
|
9400
9550
|
var LocalBackend = class {
|
|
@@ -9407,12 +9557,12 @@ var LocalBackend = class {
|
|
|
9407
9557
|
this.id = options.id;
|
|
9408
9558
|
this.sandboxMode = options.sandboxMode ?? "workspace-write";
|
|
9409
9559
|
this.#maxReadBytes = options.maxReadBytes ?? 16 * 1024 * 1024;
|
|
9410
|
-
this.#statEntry = options.statEntry ?? ((path) =>
|
|
9560
|
+
this.#statEntry = options.statEntry ?? ((path) => stat2(path));
|
|
9411
9561
|
}
|
|
9412
9562
|
async stat(absolute, signal) {
|
|
9413
9563
|
signal?.throwIfAborted();
|
|
9414
9564
|
try {
|
|
9415
|
-
const info = await
|
|
9565
|
+
const info = await stat2(absolute);
|
|
9416
9566
|
return {
|
|
9417
9567
|
exists: true,
|
|
9418
9568
|
isDirectory: info.isDirectory(),
|
|
@@ -10069,7 +10219,9 @@ function buildListScript(absolute) {
|
|
|
10069
10219
|
`if [ ! -d "$p" ]; then printf 'DSH_ERROR=not-a-dir\\n'; exit 4; fi`,
|
|
10070
10220
|
// GNU find (Linux). `-printf ''` is the cheap capability probe: it fails on BSD/macOS find.
|
|
10071
10221
|
`if find "$p" -maxdepth 1 -printf '' >/dev/null 2>&1; then`,
|
|
10072
|
-
|
|
10222
|
+
// **NUL 分隔**:文件名可以含换行(POSIX 合法,`git checkout` 就能造出来),按行切会把一个条目拆成
|
|
10223
|
+
// 两行、解析出一个不存在的名字并丢掉真条目(审计 2026-11-06 第 3 轮 P1)。
|
|
10224
|
+
` find "$p" -mindepth 1 -maxdepth 1 -printf '%y\\t%Y\\t%s\\t%T@\\t%f\\0'`,
|
|
10073
10225
|
`else`,
|
|
10074
10226
|
` for entry in "$p"/* "$p"/.[!.]*; do`,
|
|
10075
10227
|
` [ -e "$entry" ] || continue`,
|
|
@@ -10077,28 +10229,28 @@ function buildListScript(absolute) {
|
|
|
10077
10229
|
` if [ -d "$entry" ]; then kind=d; elif [ -L "$entry" ]; then kind=l; else kind=f; fi`,
|
|
10078
10230
|
` size=$(wc -c < "$entry" 2>/dev/null | tr -d ' ' || echo 0)`,
|
|
10079
10231
|
` 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\\
|
|
10232
|
+
` printf '%s\\t%s\\t%s\\t%s\\t%s\\0' "$kind" "$kind" "$size" "$mtime" "$name"`,
|
|
10081
10233
|
` done`,
|
|
10082
10234
|
`fi`
|
|
10083
10235
|
].join("\n");
|
|
10084
10236
|
}
|
|
10085
10237
|
function parseListing(stdout) {
|
|
10086
10238
|
const entries = [];
|
|
10087
|
-
for (const
|
|
10088
|
-
if (
|
|
10089
|
-
const parts =
|
|
10239
|
+
for (const record2 of stdout.split("\0")) {
|
|
10240
|
+
if (record2 === "" || record2.startsWith("DSH_")) continue;
|
|
10241
|
+
const parts = record2.split(" ");
|
|
10090
10242
|
if (parts.length < 5) continue;
|
|
10091
10243
|
const [typeChar, targetChar, sizeText, mtimeText] = parts;
|
|
10092
10244
|
const kind = typeChar === "d" ? "d" : typeChar === "l" ? targetChar === "d" ? "d" : "l" : "f";
|
|
10093
10245
|
const size = Number.parseInt(sizeText, 10);
|
|
10094
|
-
const seconds = Number.
|
|
10246
|
+
const seconds = Number.parseFloat(mtimeText);
|
|
10095
10247
|
entries.push({
|
|
10096
10248
|
name: parts.slice(4).join(" "),
|
|
10097
10249
|
isDirectory: kind === "d",
|
|
10098
10250
|
isFile: kind === "f",
|
|
10099
10251
|
isSymlink: kind === "l",
|
|
10100
10252
|
size: Number.isFinite(size) ? size : 0,
|
|
10101
|
-
mtimeMs:
|
|
10253
|
+
mtimeMs: Number.isFinite(seconds) ? Math.trunc(seconds * 1e3) : 0
|
|
10102
10254
|
});
|
|
10103
10255
|
}
|
|
10104
10256
|
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
@@ -10486,6 +10638,13 @@ var RemoteBackend = class {
|
|
|
10486
10638
|
if (result.stdout.includes("DSH_ERROR=not-a-dir")) {
|
|
10487
10639
|
throw new SshDevError("REMOTE_NOT_FOUND", `\u8FDC\u7AEF\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${absolute}`, { path: absolute });
|
|
10488
10640
|
}
|
|
10641
|
+
if (result.truncated === true) {
|
|
10642
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u76EE\u5F55\u6761\u76EE\u8FC7\u591A\uFF0C\u5217\u4E3E\u7ED3\u679C\u88AB\u622A\u65AD\uFF1A${absolute}`, {
|
|
10643
|
+
path: absolute,
|
|
10644
|
+
reason: "listing-truncated",
|
|
10645
|
+
maxBytes: 4 * 1024 * 1024
|
|
10646
|
+
});
|
|
10647
|
+
}
|
|
10489
10648
|
return parseListing(result.stdout);
|
|
10490
10649
|
}
|
|
10491
10650
|
/**
|
|
@@ -11127,7 +11286,21 @@ var RemoteBackend = class {
|
|
|
11127
11286
|
}
|
|
11128
11287
|
const command = tool === "sha256sum" ? "sha256sum" : "shasum -a 256";
|
|
11129
11288
|
const result = await (await this.#transport()).exec({ argv: ["sh", "-c", `${command} -- "$1" | cut -d' ' -f1`, "dsh-digest", absolute] }, signal);
|
|
11130
|
-
|
|
11289
|
+
if (result.exitCode !== 0) {
|
|
11290
|
+
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}`, {
|
|
11291
|
+
path: absolute,
|
|
11292
|
+
exitCode: result.exitCode,
|
|
11293
|
+
stderr: result.stderr.slice(0, 200)
|
|
11294
|
+
});
|
|
11295
|
+
}
|
|
11296
|
+
const digest = result.stdout.trim();
|
|
11297
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) {
|
|
11298
|
+
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}`, {
|
|
11299
|
+
path: absolute,
|
|
11300
|
+
digest: digest.slice(0, 120)
|
|
11301
|
+
});
|
|
11302
|
+
}
|
|
11303
|
+
return digest;
|
|
11131
11304
|
}
|
|
11132
11305
|
};
|
|
11133
11306
|
function sha256Hex(bytes) {
|
|
@@ -11958,12 +12131,12 @@ init_ssh2_transport();
|
|
|
11958
12131
|
|
|
11959
12132
|
// src/host/integration/agent/agent-install-io.ts
|
|
11960
12133
|
init_shell_quote();
|
|
11961
|
-
import { readFile as readFile2, stat as
|
|
12134
|
+
import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
|
|
11962
12135
|
var DEFAULT_MAX_UPLOAD_BYTES = 32 * 1024 * 1024;
|
|
11963
12136
|
function createAgentInstallIo(options) {
|
|
11964
12137
|
const maxUploadBytes = options.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
11965
12138
|
const readLocal = options.readLocalFile ?? ((path) => readFile2(path).then((buffer) => new Uint8Array(buffer)));
|
|
11966
|
-
const sizeOf = options.localSize ?? ((path) =>
|
|
12139
|
+
const sizeOf = options.localSize ?? ((path) => stat3(path).then((info) => info.size));
|
|
11967
12140
|
const exec = async (command, signal) => {
|
|
11968
12141
|
const result = await options.transport.exec(
|
|
11969
12142
|
{ argv: ["/bin/sh", "-c", command], maxBytes: options.probeMaxBytes ?? 256 * 1024 },
|
|
@@ -12046,14 +12219,14 @@ function reviseToTier1(profile, decision, reason, hint) {
|
|
|
12046
12219
|
// src/host/integration/agent/askpass-registry.ts
|
|
12047
12220
|
function createAskpassRegistry(options) {
|
|
12048
12221
|
const handles = /* @__PURE__ */ new Map();
|
|
12049
|
-
const
|
|
12222
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
12050
12223
|
const retired = /* @__PURE__ */ new Set();
|
|
12051
12224
|
const prepare = async (profile) => {
|
|
12052
12225
|
if (!needsAskpass(profile.auth.kind)) return void 0;
|
|
12053
12226
|
if (retired.has(profile.id)) return void 0;
|
|
12054
12227
|
const existing = handles.get(profile.id);
|
|
12055
12228
|
if (existing !== void 0) return existing.env;
|
|
12056
|
-
const inflight =
|
|
12229
|
+
const inflight = pending2.get(profile.id);
|
|
12057
12230
|
if (inflight !== void 0) return inflight;
|
|
12058
12231
|
const attempt = (async () => {
|
|
12059
12232
|
try {
|
|
@@ -12070,16 +12243,16 @@ function createAskpassRegistry(options) {
|
|
|
12070
12243
|
return void 0;
|
|
12071
12244
|
}
|
|
12072
12245
|
})().finally(() => {
|
|
12073
|
-
|
|
12246
|
+
pending2.delete(profile.id);
|
|
12074
12247
|
});
|
|
12075
|
-
|
|
12248
|
+
pending2.set(profile.id, attempt);
|
|
12076
12249
|
return attempt;
|
|
12077
12250
|
};
|
|
12078
12251
|
const forget = async (connectionId) => {
|
|
12079
12252
|
retired.add(connectionId);
|
|
12080
12253
|
const handle = handles.get(connectionId);
|
|
12081
12254
|
handles.delete(connectionId);
|
|
12082
|
-
|
|
12255
|
+
pending2.delete(connectionId);
|
|
12083
12256
|
if (handle !== void 0) await handle.dispose().catch(() => void 0);
|
|
12084
12257
|
};
|
|
12085
12258
|
return {
|
|
@@ -12097,7 +12270,7 @@ function createAskpassRegistry(options) {
|
|
|
12097
12270
|
retired.delete(connectionId);
|
|
12098
12271
|
},
|
|
12099
12272
|
disposeAll: async () => {
|
|
12100
|
-
const ids = /* @__PURE__ */ new Set([...handles.keys(), ...
|
|
12273
|
+
const ids = /* @__PURE__ */ new Set([...handles.keys(), ...pending2.keys()]);
|
|
12101
12274
|
for (const id of ids) retired.add(id);
|
|
12102
12275
|
await Promise.all([...ids].map((id) => forget(id)));
|
|
12103
12276
|
}
|
|
@@ -12482,7 +12655,7 @@ function directoryProbeCommand(root) {
|
|
|
12482
12655
|
return `test -d ${shellQuote(root)}`;
|
|
12483
12656
|
}
|
|
12484
12657
|
function rankMatches(paths, query, limit) {
|
|
12485
|
-
const needle = query.trim().toLowerCase();
|
|
12658
|
+
const needle = separatorsForMatching(query.trim().toLowerCase());
|
|
12486
12659
|
if (needle === "") {
|
|
12487
12660
|
const top = [];
|
|
12488
12661
|
const better = (left, right) => {
|
|
@@ -12507,7 +12680,8 @@ function rankMatches(paths, query, limit) {
|
|
|
12507
12680
|
}
|
|
12508
12681
|
const scored = [];
|
|
12509
12682
|
for (const path of paths) {
|
|
12510
|
-
const
|
|
12683
|
+
const normalized = separatorsForMatching(path);
|
|
12684
|
+
const lower = normalized.toLowerCase();
|
|
12511
12685
|
const base = lower.slice(lower.lastIndexOf("/") + 1);
|
|
12512
12686
|
let score;
|
|
12513
12687
|
if (base.startsWith(needle)) score = 0;
|
|
@@ -12522,11 +12696,15 @@ function rankMatches(paths, query, limit) {
|
|
|
12522
12696
|
);
|
|
12523
12697
|
return scored.slice(0, limit).map((entry) => entry.path);
|
|
12524
12698
|
}
|
|
12699
|
+
function separatorsForMatching(path) {
|
|
12700
|
+
return path.includes("\\") && !path.includes("/") ? path.replaceAll("\\", "/") : path;
|
|
12701
|
+
}
|
|
12525
12702
|
function depth2(path) {
|
|
12526
12703
|
let segments = 0;
|
|
12527
12704
|
let inSegment = false;
|
|
12528
12705
|
for (let index = 0; index < path.length; index += 1) {
|
|
12529
|
-
|
|
12706
|
+
const code = path.charCodeAt(index);
|
|
12707
|
+
if (code === 47 || code === 92) {
|
|
12530
12708
|
inSegment = false;
|
|
12531
12709
|
continue;
|
|
12532
12710
|
}
|
|
@@ -13455,12 +13633,8 @@ function wireConnections(ctx, config) {
|
|
|
13455
13633
|
}
|
|
13456
13634
|
function readLiveSessions(ctx) {
|
|
13457
13635
|
let list;
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
list = sessions?.list?.();
|
|
13461
|
-
} catch {
|
|
13462
|
-
return [];
|
|
13463
|
-
}
|
|
13636
|
+
const sessions = ctx.get("sessions");
|
|
13637
|
+
list = sessions?.list?.();
|
|
13464
13638
|
if (!Array.isArray(list)) return [];
|
|
13465
13639
|
const live = [];
|
|
13466
13640
|
for (const session of list) {
|
|
@@ -13482,9 +13656,9 @@ import { bindTypertRemote } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
13482
13656
|
// src/shared/remote-identity.ts
|
|
13483
13657
|
var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
13484
13658
|
var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
13659
|
+
var SSH_DEV_REMOTE_METHOD = "connections";
|
|
13485
13660
|
|
|
13486
13661
|
// src/host/integration/remote/remote-face.ts
|
|
13487
|
-
var SSH_DEV_REMOTE_CONNECTIONS_METHOD = "connections";
|
|
13488
13662
|
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
13663
|
function buildConnectionStatusRows(source) {
|
|
13490
13664
|
return source.profiles().map((profile) => {
|
|
@@ -13549,7 +13723,7 @@ var SshDevRemoteFace = class extends Service {
|
|
|
13549
13723
|
return buildConnectionStatusRows(this.#source);
|
|
13550
13724
|
}
|
|
13551
13725
|
};
|
|
13552
|
-
markRemoteMethod(SshDevRemoteFace.prototype,
|
|
13726
|
+
markRemoteMethod(SshDevRemoteFace.prototype, SSH_DEV_REMOTE_METHOD);
|
|
13553
13727
|
function installRemoteFace(ctx) {
|
|
13554
13728
|
return new SshDevRemoteFace(ctx, {
|
|
13555
13729
|
profiles: () => ctx.get("sshDevConnections")?.list() ?? [],
|
|
@@ -13636,6 +13810,8 @@ function sessionToolDefinitions(service) {
|
|
|
13636
13810
|
sessions: { type: "array", items: { type: "string" }, description: "`list`: one formatted line per session." },
|
|
13637
13811
|
entries: { type: "array", items: { type: "string" }, description: "`restore` without an id: bin entry ids." },
|
|
13638
13812
|
restoredPath: { type: "string", description: "`restore` with an id: where the entry went back to." },
|
|
13813
|
+
restoredSubsessions: { type: "integer", description: "`restore`: sub-sessions restored together with the entry." },
|
|
13814
|
+
failedSubsessions: { type: "integer", description: "`restore`: sub-sessions that stayed in the bin (path occupied / move failed)." },
|
|
13639
13815
|
matched: { type: "integer", description: "`delete_workspace`: how many sessions belonged to the directory." },
|
|
13640
13816
|
mode: { type: "string", description: "`delete` / `delete_workspace`: the mode that was applied." },
|
|
13641
13817
|
recoverable: { type: "boolean", description: "`delete`: whether the bin can bring it back." },
|
|
@@ -13671,10 +13847,16 @@ function sessionToolDefinitions(service) {
|
|
|
13671
13847
|
}
|
|
13672
13848
|
if (typeof entryId !== "string" || entryId === "") throw new Error("entryId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u7701\u7565\u5219\u5217\u51FA\u56DE\u6536\u7AD9\uFF09");
|
|
13673
13849
|
const result = await service.restore(entryId);
|
|
13850
|
+
const extra = [];
|
|
13851
|
+
if (result.restoredSubsessions.length > 0) extra.push(`\u5E76\u8FD8\u539F\u5B50\u4F1A\u8BDD ${result.restoredSubsessions.length} \u4E2A`);
|
|
13852
|
+
if (result.failedSubsessions.length > 0) extra.push(`${result.failedSubsessions.length} \u4E2A\u5B50\u4F1A\u8BDD\u4ECD\u5728\u56DE\u6536\u7AD9`);
|
|
13853
|
+
if (result.parentStillBinned) extra.push("\u5176\u7236\u4F1A\u8BDD\u4ECD\u5728\u56DE\u6536\u7AD9");
|
|
13674
13854
|
return {
|
|
13675
13855
|
action: "restore",
|
|
13676
13856
|
restoredPath: result.restoredPath,
|
|
13677
|
-
|
|
13857
|
+
restoredSubsessions: result.restoredSubsessions.length,
|
|
13858
|
+
failedSubsessions: result.failedSubsessions.length,
|
|
13859
|
+
summary: `\u5DF2\u8FD8\u539F\u4F1A\u8BDD ${result.entry.sessionId} -> ${result.restoredPath}${extra.length === 0 ? "" : `\uFF08${extra.join("\uFF0C")}\uFF09`}`
|
|
13678
13860
|
};
|
|
13679
13861
|
}
|
|
13680
13862
|
const mode = record2["mode"] ?? "recycle";
|
|
@@ -13753,7 +13935,7 @@ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
|
13753
13935
|
init_errors();
|
|
13754
13936
|
|
|
13755
13937
|
// 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";
|
|
13938
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename3, stat as stat4, unlink as unlink3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13757
13939
|
import { join as join12 } from "node:path";
|
|
13758
13940
|
init_errors();
|
|
13759
13941
|
var USER_ROOTS_VERSION = 1;
|
|
@@ -13779,6 +13961,10 @@ var nodeUserRootsIo = {
|
|
|
13779
13961
|
await mkdir4(path, { recursive: true, mode: 448 });
|
|
13780
13962
|
},
|
|
13781
13963
|
rename: (from, to) => rename3(from, to),
|
|
13964
|
+
stat: async (path) => {
|
|
13965
|
+
const info = await stat4(path);
|
|
13966
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
13967
|
+
},
|
|
13782
13968
|
unlink: async (path) => {
|
|
13783
13969
|
try {
|
|
13784
13970
|
await unlink3(path);
|
|
@@ -14141,7 +14327,19 @@ function createUserRootsStore(options) {
|
|
|
14141
14327
|
const filePath = join12(options.dir, USER_ROOTS_FILE_NAME);
|
|
14142
14328
|
let cachedRaw;
|
|
14143
14329
|
let cachedLoad;
|
|
14330
|
+
let cachedSignature;
|
|
14331
|
+
const signatureOf = async () => {
|
|
14332
|
+
if (io.stat === void 0) return void 0;
|
|
14333
|
+
try {
|
|
14334
|
+
const info = await io.stat(filePath);
|
|
14335
|
+
return `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
14336
|
+
} catch {
|
|
14337
|
+
return void 0;
|
|
14338
|
+
}
|
|
14339
|
+
};
|
|
14144
14340
|
const load = async () => {
|
|
14341
|
+
const signature = await signatureOf();
|
|
14342
|
+
if (signature !== void 0 && signature === cachedSignature && cachedLoad !== void 0) return cachedLoad;
|
|
14145
14343
|
let text8;
|
|
14146
14344
|
try {
|
|
14147
14345
|
text8 = await io.readFile(filePath);
|
|
@@ -14153,10 +14351,14 @@ function createUserRootsStore(options) {
|
|
|
14153
14351
|
degraded: true
|
|
14154
14352
|
};
|
|
14155
14353
|
}
|
|
14156
|
-
if (cachedRaw !== void 0 && text8 === cachedRaw && cachedLoad !== void 0)
|
|
14354
|
+
if (cachedRaw !== void 0 && text8 === cachedRaw && cachedLoad !== void 0) {
|
|
14355
|
+
cachedSignature = signature;
|
|
14356
|
+
return cachedLoad;
|
|
14357
|
+
}
|
|
14157
14358
|
const parsed = parseUserRoots(text8);
|
|
14158
14359
|
cachedRaw = text8;
|
|
14159
14360
|
cachedLoad = parsed;
|
|
14361
|
+
cachedSignature = signature;
|
|
14160
14362
|
return parsed;
|
|
14161
14363
|
};
|
|
14162
14364
|
const write = async (next, preparedText) => {
|
|
@@ -14173,6 +14375,7 @@ function createUserRootsStore(options) {
|
|
|
14173
14375
|
}
|
|
14174
14376
|
cachedRaw = text8;
|
|
14175
14377
|
cachedLoad = { state: validated, warnings: [], degraded: false };
|
|
14378
|
+
cachedSignature = void 0;
|
|
14176
14379
|
return validated;
|
|
14177
14380
|
};
|
|
14178
14381
|
const queue = createSerialQueue();
|
|
@@ -14404,6 +14607,9 @@ var STATUS_BY_CODE = {
|
|
|
14404
14607
|
ROOT_INVALID: 400,
|
|
14405
14608
|
CONNECTION_INVALID: 400,
|
|
14406
14609
|
CONNECTION_NOT_FOUND: 400,
|
|
14610
|
+
// 路径不在任何已注册的根内 / 根没有注册:客户端换一个路径或先加根就能成功,不是服务器故障。
|
|
14611
|
+
// 它此前漏在表外,于是**所有**经 HTTP 暴露的根外访问都报 500(审计 2026-11-06 第 4 轮 P1)。
|
|
14612
|
+
ROOT_OUTSIDE: 400,
|
|
14407
14613
|
// 目标不存在 / 状态冲突 / 本组合不支持:客户端可以据此换一条路。
|
|
14408
14614
|
TRASH_NOT_FOUND: 404,
|
|
14409
14615
|
TRANSFER_CONFLICT: 409,
|
|
@@ -14446,6 +14652,9 @@ function optionalPath(value) {
|
|
|
14446
14652
|
reason: "invalid-path"
|
|
14447
14653
|
});
|
|
14448
14654
|
}
|
|
14655
|
+
if (/[\u0000-\u001f]/.test(value)) {
|
|
14656
|
+
throw new SshDevError("ROOT_INVALID", "path \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\u542B NUL\uFF09", { reason: "invalid-path" });
|
|
14657
|
+
}
|
|
14449
14658
|
return value.trim();
|
|
14450
14659
|
}
|
|
14451
14660
|
function pick(source, fields) {
|
|
@@ -14492,6 +14701,15 @@ function connectionIdFromAlias(alias) {
|
|
|
14492
14701
|
// src/host/app/roots/session-directories.ts
|
|
14493
14702
|
var SESSION_DIRECTORY_MAX_ROOTS = 32;
|
|
14494
14703
|
var SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14704
|
+
var REMOTE_SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14705
|
+
var REMOTE_BROAD_ROOTS = [...REMOTE_SYSTEM_TREES, "/home", "/opt", "/srv", "/mnt", "/media", "/tmp"];
|
|
14706
|
+
function remotePathTooBroad(normalized) {
|
|
14707
|
+
if (REMOTE_BROAD_ROOTS.includes(normalized)) return true;
|
|
14708
|
+
for (const tree of REMOTE_SYSTEM_TREES) {
|
|
14709
|
+
if (normalized === tree || normalized.startsWith(`${tree}/`)) return true;
|
|
14710
|
+
}
|
|
14711
|
+
return false;
|
|
14712
|
+
}
|
|
14495
14713
|
function directoryRefusal(request) {
|
|
14496
14714
|
const trimmed = request.path.trim();
|
|
14497
14715
|
if (trimmed === "") return "\u76EE\u5F55\u8DEF\u5F84\u4E3A\u7A7A\u3002";
|
|
@@ -14507,6 +14725,9 @@ function directoryRefusal(request) {
|
|
|
14507
14725
|
return error instanceof Error ? error.message : `\u8FDC\u7AEF\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF1A${trimmed}`;
|
|
14508
14726
|
}
|
|
14509
14727
|
if (normalized2 === "/") return "\u4E0D\u5141\u8BB8\u628A\u8FDC\u7AEF\u7684\u6839\u76EE\u5F55 / \u6DFB\u52A0\u5230\u4F1A\u8BDD\u3002";
|
|
14728
|
+
if (remotePathTooBroad(normalized2)) {
|
|
14729
|
+
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`;
|
|
14730
|
+
}
|
|
14510
14731
|
return void 0;
|
|
14511
14732
|
}
|
|
14512
14733
|
const normalized = trimmed.replace(/\/+$/u, "") || "/";
|
|
@@ -14644,6 +14865,15 @@ async function ensureLiveAndReadAnchor(deps, key, state) {
|
|
|
14644
14865
|
}
|
|
14645
14866
|
return readAnchor(await Promise.resolve(deps.listRoots()));
|
|
14646
14867
|
}
|
|
14868
|
+
async function liveScopeWarning(deps, key, state) {
|
|
14869
|
+
const persisted = findRootInState(state, key);
|
|
14870
|
+
if (persisted === void 0) return void 0;
|
|
14871
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14872
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14873
|
+
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`;
|
|
14874
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return void 0;
|
|
14875
|
+
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`;
|
|
14876
|
+
}
|
|
14647
14877
|
async function findLiveKey(deps, entry) {
|
|
14648
14878
|
for (const raw of await Promise.resolve(deps.listRoots())) {
|
|
14649
14879
|
const key = liveKeyOfRecord(raw, entry);
|
|
@@ -14910,9 +15140,25 @@ function publicRoot(raw) {
|
|
|
14910
15140
|
...typeof source["connectionId"] === "string" ? { connectionId: source["connectionId"] } : {},
|
|
14911
15141
|
path: String(source["path"] ?? ""),
|
|
14912
15142
|
...typeof source["label"] === "string" ? { label: source["label"] } : {},
|
|
14913
|
-
...typeof source["writable"] === "boolean" ? { writable: source["writable"] } : {}
|
|
15143
|
+
...typeof source["writable"] === "boolean" ? { writable: source["writable"] } : {},
|
|
15144
|
+
...stringList(source["workspaces"]) === void 0 ? {} : { workspaces: stringList(source["workspaces"]) },
|
|
15145
|
+
...stringList(source["sessions"]) === void 0 ? {} : { sessions: stringList(source["sessions"]) }
|
|
14914
15146
|
};
|
|
14915
15147
|
}
|
|
15148
|
+
function liveRootScopes(raw) {
|
|
15149
|
+
const outer = asRecord2(raw) ?? {};
|
|
15150
|
+
const source = asRecord2(outer["root"]) ?? outer;
|
|
15151
|
+
const sessions = stringList(source["sessions"]);
|
|
15152
|
+
const workspaces = stringList(source["workspaces"]);
|
|
15153
|
+
return {
|
|
15154
|
+
...sessions === void 0 ? {} : { sessions },
|
|
15155
|
+
...workspaces === void 0 ? {} : { workspaces }
|
|
15156
|
+
};
|
|
15157
|
+
}
|
|
15158
|
+
function stringList(value) {
|
|
15159
|
+
if (!Array.isArray(value)) return void 0;
|
|
15160
|
+
return value.filter((item) => typeof item === "string");
|
|
15161
|
+
}
|
|
14916
15162
|
function publicConnection(raw) {
|
|
14917
15163
|
const source = asRecord2(raw) ?? {};
|
|
14918
15164
|
const authKind = source["authKind"] ?? asRecord2(source["auth"])?.["kind"];
|
|
@@ -15038,21 +15284,34 @@ async function handleRootActions(action, body, req, res, deps) {
|
|
|
15038
15284
|
if (registeredNow) await deps.unregisterRoot(key);
|
|
15039
15285
|
throw error;
|
|
15040
15286
|
}
|
|
15287
|
+
const warnings = [];
|
|
15288
|
+
if (existing !== void 0) {
|
|
15289
|
+
const warning = await liveScopeWarning(deps, key, state);
|
|
15290
|
+
if (warning !== void 0) warnings.push(warning);
|
|
15291
|
+
}
|
|
15041
15292
|
const anchorPath = await anchorPathOf(deps, entry);
|
|
15042
15293
|
send(req, res, 200, {
|
|
15043
15294
|
ok: true,
|
|
15044
15295
|
action,
|
|
15045
15296
|
state: publicUserState(state),
|
|
15046
|
-
...anchorPath === void 0 ? {} : { anchorPath }
|
|
15297
|
+
...anchorPath === void 0 ? {} : { anchorPath },
|
|
15298
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
15047
15299
|
});
|
|
15048
15300
|
return true;
|
|
15049
|
-
return true;
|
|
15050
15301
|
});
|
|
15051
15302
|
}
|
|
15052
15303
|
case "remove-root": {
|
|
15053
15304
|
const key = requiredKey(body["key"]);
|
|
15054
15305
|
const workspace = optionalWorkspaceName(body["workspace"]);
|
|
15055
15306
|
const session = optionalSessionName(body["session"]);
|
|
15307
|
+
if (session !== void 0) {
|
|
15308
|
+
const current = await currentUserState(deps);
|
|
15309
|
+
const entry = findRootInState(current, key);
|
|
15310
|
+
const owners = entry?.sessions ?? [];
|
|
15311
|
+
if (owners.length > 0 && !owners.includes(session)) {
|
|
15312
|
+
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 });
|
|
15313
|
+
}
|
|
15314
|
+
}
|
|
15056
15315
|
const state = await reconcile(deps, (current) => {
|
|
15057
15316
|
if (session !== void 0 && rootAddedBySession(current, key, session)) {
|
|
15058
15317
|
return removeRootFromSession(current, key, session);
|
|
@@ -15734,16 +15993,40 @@ async function serveSnapshot(req, res, deps) {
|
|
|
15734
15993
|
Promise.resolve(deps.listConnections()),
|
|
15735
15994
|
Promise.resolve(deps.loadUserState())
|
|
15736
15995
|
]);
|
|
15996
|
+
const query = queryOf(req.url);
|
|
15997
|
+
const requestedSession = (query.get("session") ?? "").trim();
|
|
15998
|
+
const requestedWorkspace = (query.get("workspace") ?? "").trim();
|
|
15999
|
+
const ancestorsOf = deps.resolveSessionAncestors;
|
|
16000
|
+
const canFilter = requestedWorkspace !== "" || ancestorsOf !== void 0;
|
|
16001
|
+
const filtered = !canFilter || requestedSession === "" && requestedWorkspace === "" ? void 0 : {
|
|
16002
|
+
ids: new Set(
|
|
16003
|
+
requestedSession === "" ? [] : [requestedSession, ...ancestorsOf?.(requestedSession) ?? []]
|
|
16004
|
+
),
|
|
16005
|
+
workspace: requestedWorkspace
|
|
16006
|
+
};
|
|
16007
|
+
const roots = filtered === void 0 ? liveRoots.map(publicRoot) : liveRoots.filter((raw) => visibleToSession(liveRootScopes(raw), filtered.ids, filtered.workspace)).map(publicRoot);
|
|
15737
16008
|
send(req, res, 200, {
|
|
15738
16009
|
ok: true,
|
|
16010
|
+
// 两个标记分开写:客户端只认 `scopedForSession`,而只带 workspace 的调用方也能看出"这份列表被过滤过"。
|
|
16011
|
+
...filtered === void 0 ? {} : requestedSession === "" ? { scopedForWorkspace: requestedWorkspace } : { scopedForSession: requestedSession },
|
|
15739
16012
|
// The plugin's **anchor root**, not the roots' individual anchor paths: see `RootsApiDeps.anchorRootDir`.
|
|
15740
16013
|
...deps.anchorRootDir === void 0 ? {} : { anchorRoot: deps.anchorRootDir },
|
|
15741
|
-
roots
|
|
16014
|
+
roots,
|
|
15742
16015
|
connections: liveConnections.map(publicConnection),
|
|
15743
16016
|
userRoots: publicUserState(loaded.state),
|
|
15744
16017
|
warnings: [...loaded.warnings]
|
|
15745
16018
|
});
|
|
15746
16019
|
}
|
|
16020
|
+
function queryOf(url) {
|
|
16021
|
+
const index = url === void 0 ? -1 : url.indexOf("?");
|
|
16022
|
+
return new URLSearchParams(index === -1 || url === void 0 ? "" : url.slice(index + 1));
|
|
16023
|
+
}
|
|
16024
|
+
function visibleToSession(row, sessionIds, workspace) {
|
|
16025
|
+
const sessions = row.sessions ?? [];
|
|
16026
|
+
if (sessions.length > 0) return sessions.some((id) => sessionIds.has(id));
|
|
16027
|
+
if (workspace === "") return false;
|
|
16028
|
+
return (row.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, workspace));
|
|
16029
|
+
}
|
|
15747
16030
|
async function serveAction(req, res, deps) {
|
|
15748
16031
|
let text8;
|
|
15749
16032
|
try {
|
|
@@ -15799,7 +16082,7 @@ async function serveAction(req, res, deps) {
|
|
|
15799
16082
|
|
|
15800
16083
|
// src/host/integration/session/session-bin-lock.ts
|
|
15801
16084
|
init_errors();
|
|
15802
|
-
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as
|
|
16085
|
+
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as stat5, utimes } from "node:fs/promises";
|
|
15803
16086
|
import { dirname as dirname5 } from "node:path";
|
|
15804
16087
|
var DEFAULT_STALE_LOCK_MS = 6e4;
|
|
15805
16088
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
@@ -15808,6 +16091,7 @@ function createSessionBinLock(options) {
|
|
|
15808
16091
|
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
15809
16092
|
const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;
|
|
15810
16093
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
16094
|
+
const heartbeatMs = options.heartbeatMs ?? (staleMs > 0 ? Math.max(1e3, Math.floor(staleMs / 4)) : 0);
|
|
15811
16095
|
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
15812
16096
|
const now = options.now ?? (() => Date.now());
|
|
15813
16097
|
const owner = options.owner ?? `${process.pid}@${hostnameOf()}`;
|
|
@@ -15857,22 +16141,47 @@ function createSessionBinLock(options) {
|
|
|
15857
16141
|
const reclaimIfStale = async () => {
|
|
15858
16142
|
if (staleMs <= 0) return false;
|
|
15859
16143
|
let age;
|
|
16144
|
+
let malformed = false;
|
|
15860
16145
|
try {
|
|
15861
|
-
const info = await
|
|
16146
|
+
const info = await stat5(options.path);
|
|
16147
|
+
malformed = info.isDirectory();
|
|
15862
16148
|
age = now() - info.mtimeMs;
|
|
15863
16149
|
} catch {
|
|
15864
16150
|
return true;
|
|
15865
16151
|
}
|
|
15866
|
-
if (age < staleMs) return false;
|
|
16152
|
+
if (!malformed && age < staleMs) return false;
|
|
16153
|
+
if (!malformed) {
|
|
16154
|
+
try {
|
|
16155
|
+
const text8 = await readFile6(options.path, "utf8");
|
|
16156
|
+
const record2 = JSON.parse(text8);
|
|
16157
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: record2 });
|
|
16158
|
+
} catch {
|
|
16159
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: {} });
|
|
16160
|
+
}
|
|
16161
|
+
} else {
|
|
16162
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: {} });
|
|
16163
|
+
}
|
|
16164
|
+
try {
|
|
16165
|
+
await rm3(options.path, { force: true, recursive: malformed });
|
|
16166
|
+
} catch (error) {
|
|
16167
|
+
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}`, {
|
|
16168
|
+
path: options.path,
|
|
16169
|
+
reason: "malformed-lock",
|
|
16170
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
16171
|
+
});
|
|
16172
|
+
}
|
|
16173
|
+
return true;
|
|
16174
|
+
};
|
|
16175
|
+
const refreshLock = async (token) => {
|
|
15867
16176
|
try {
|
|
15868
16177
|
const text8 = await readFile6(options.path, "utf8");
|
|
15869
16178
|
const record2 = JSON.parse(text8);
|
|
15870
|
-
|
|
15871
|
-
|
|
15872
|
-
|
|
16179
|
+
if (record2.token !== token) return;
|
|
16180
|
+
const at = new Date(now());
|
|
16181
|
+
await utimes(options.path, at, at);
|
|
16182
|
+
} catch (error) {
|
|
16183
|
+
options.onHeartbeatError?.(error);
|
|
15873
16184
|
}
|
|
15874
|
-
await rm3(options.path, { force: true });
|
|
15875
|
-
return true;
|
|
15876
16185
|
};
|
|
15877
16186
|
const withLock = async (_key, operation) => {
|
|
15878
16187
|
const deadline = now() + timeoutMs;
|
|
@@ -15890,9 +16199,17 @@ function createSessionBinLock(options) {
|
|
|
15890
16199
|
}
|
|
15891
16200
|
await sleep2(retryMs);
|
|
15892
16201
|
}
|
|
16202
|
+
let heartbeat;
|
|
16203
|
+
if (token !== void 0 && heartbeatMs > 0) {
|
|
16204
|
+
heartbeat = setInterval(() => {
|
|
16205
|
+
void refreshLock(token);
|
|
16206
|
+
}, heartbeatMs);
|
|
16207
|
+
heartbeat.unref?.();
|
|
16208
|
+
}
|
|
15893
16209
|
try {
|
|
15894
16210
|
return await operation();
|
|
15895
16211
|
} finally {
|
|
16212
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
15896
16213
|
if (token !== void 0) await releaseIfOwned(token);
|
|
15897
16214
|
}
|
|
15898
16215
|
};
|
|
@@ -15999,6 +16316,7 @@ function createRootsWiring(deps) {
|
|
|
15999
16316
|
deps.removeConnection(id);
|
|
16000
16317
|
},
|
|
16001
16318
|
...deps.anchorRootDir === void 0 ? {} : { anchorRootDir: deps.anchorRootDir },
|
|
16319
|
+
...deps.resolveSessionAncestors === void 0 ? {} : { resolveSessionAncestors: deps.resolveSessionAncestors },
|
|
16002
16320
|
...listRemoteDirectory === void 0 ? {} : { listRemoteDirectory: (input) => listRemoteDirectory(input) },
|
|
16003
16321
|
...remoteHomeDirectory === void 0 ? {} : { remoteHomeDirectory },
|
|
16004
16322
|
...listLocalDirectory === void 0 ? {} : { listLocalDirectory: (input) => listLocalDirectory(input) },
|
|
@@ -16072,10 +16390,55 @@ function createRootsWiring(deps) {
|
|
|
16072
16390
|
}
|
|
16073
16391
|
return { roots, connections, warnings };
|
|
16074
16392
|
};
|
|
16393
|
+
const inheritAddedRoots = async (sourceSessionId, childSessionId) => {
|
|
16394
|
+
const source = sourceSessionId.trim();
|
|
16395
|
+
const child = childSessionId.trim();
|
|
16396
|
+
if (source === "" || child === "") return;
|
|
16397
|
+
const keys = [];
|
|
16398
|
+
await store2.mutate((current) => {
|
|
16399
|
+
let next = current;
|
|
16400
|
+
for (const entry of current.roots) {
|
|
16401
|
+
const sessions = entry.sessions ?? [];
|
|
16402
|
+
if (!sessions.includes(source) || sessions.includes(child)) continue;
|
|
16403
|
+
const key = userRootKey(entry);
|
|
16404
|
+
next = withSessions(next, key, [child]);
|
|
16405
|
+
keys.push(key);
|
|
16406
|
+
}
|
|
16407
|
+
return next;
|
|
16408
|
+
});
|
|
16409
|
+
if (keys.length > 0) {
|
|
16410
|
+
const { state } = await store2.load();
|
|
16411
|
+
for (const key of keys) await refreshLiveScope(apiDeps, key, state);
|
|
16412
|
+
}
|
|
16413
|
+
};
|
|
16414
|
+
const pruneSession = async (sessionId) => {
|
|
16415
|
+
const trimmed = sessionId.trim();
|
|
16416
|
+
if (trimmed === "") return { changed: [], removed: [] };
|
|
16417
|
+
const before = (await store2.load()).state;
|
|
16418
|
+
const touched = before.roots.filter((entry) => (entry.sessions ?? []).includes(trimmed));
|
|
16419
|
+
if (touched.length === 0) return { changed: [], removed: [] };
|
|
16420
|
+
const after = await store2.mutate((current) => pruneSessionFromRoots(current, trimmed));
|
|
16421
|
+
const afterKeys = new Set(after.roots.map(userRootKey));
|
|
16422
|
+
const removed = [];
|
|
16423
|
+
const changed = [];
|
|
16424
|
+
for (const entry of touched) {
|
|
16425
|
+
const key = userRootKey(entry);
|
|
16426
|
+
if (!afterKeys.has(key)) {
|
|
16427
|
+
removed.push(key);
|
|
16428
|
+
await deps.unregisterRoot(key);
|
|
16429
|
+
} else {
|
|
16430
|
+
changed.push(key);
|
|
16431
|
+
}
|
|
16432
|
+
}
|
|
16433
|
+
for (const key of changed) await refreshLiveScope(apiDeps, key, after);
|
|
16434
|
+
return { changed, removed };
|
|
16435
|
+
};
|
|
16075
16436
|
return {
|
|
16076
16437
|
api,
|
|
16077
16438
|
store: store2,
|
|
16078
16439
|
sessionDirectories,
|
|
16440
|
+
inheritAddedRoots,
|
|
16441
|
+
pruneSession,
|
|
16079
16442
|
mergeUserRoots,
|
|
16080
16443
|
routeWarnings: () => [...routeWarnings],
|
|
16081
16444
|
routeFacts: () => ({ injectRan, routeRegistered, warnings: [...routeWarnings] }),
|
|
@@ -16338,7 +16701,14 @@ async function serveActionWithSignal(action, body, req, res, deps, service, sign
|
|
|
16338
16701
|
return;
|
|
16339
16702
|
}
|
|
16340
16703
|
const result = await service.restore(entryId, options);
|
|
16341
|
-
send(req, res, 200, {
|
|
16704
|
+
send(req, res, 200, {
|
|
16705
|
+
ok: true,
|
|
16706
|
+
restoredPath: result.restoredPath,
|
|
16707
|
+
movedBy: result.movedBy,
|
|
16708
|
+
restoredSubsessions: result.restoredSubsessions.length,
|
|
16709
|
+
failedSubsessions: result.failedSubsessions.length,
|
|
16710
|
+
parentStillBinned: result.parentStillBinned
|
|
16711
|
+
});
|
|
16342
16712
|
return;
|
|
16343
16713
|
}
|
|
16344
16714
|
case "purge": {
|
|
@@ -16352,7 +16722,11 @@ async function serveActionWithSignal(action, body, req, res, deps, service, sign
|
|
|
16352
16722
|
return;
|
|
16353
16723
|
}
|
|
16354
16724
|
const result = await service.purge(entryId, options);
|
|
16355
|
-
send(req, res, 200, {
|
|
16725
|
+
send(req, res, 200, {
|
|
16726
|
+
...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 },
|
|
16727
|
+
purged: result.purged,
|
|
16728
|
+
indexStale: result.indexStale
|
|
16729
|
+
});
|
|
16356
16730
|
return;
|
|
16357
16731
|
}
|
|
16358
16732
|
case "sweep": {
|
|
@@ -16365,8 +16739,9 @@ async function serveActionWithSignal(action, body, req, res, deps, service, sign
|
|
|
16365
16739
|
return;
|
|
16366
16740
|
}
|
|
16367
16741
|
const result = await service.sweep(deps.policy(), options);
|
|
16742
|
+
const stale = result.purged.filter((item) => item.indexStale === true).length;
|
|
16368
16743
|
send(req, res, 200, {
|
|
16369
|
-
ok: true,
|
|
16744
|
+
...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
16745
|
purged: result.purged.map((item) => ({ entryId: String(item.id), sessionId: item.sessionId, indexStale: item.indexStale })),
|
|
16371
16746
|
failed: result.failed.map((item) => ({ entryId: String(item.id), reason: item.reason })),
|
|
16372
16747
|
remainingEntries: result.remainingEntries,
|
|
@@ -16441,6 +16816,56 @@ function createSessionRouteWiring(deps) {
|
|
|
16441
16816
|
};
|
|
16442
16817
|
}
|
|
16443
16818
|
|
|
16819
|
+
// src/host/integration/roots/fork-inheritance.ts
|
|
16820
|
+
var PATCHED = /* @__PURE__ */ Symbol.for("dsh-over-ssh/fork-inheritance");
|
|
16821
|
+
function sourceIdOf(source) {
|
|
16822
|
+
if (typeof source === "string") return source === "" ? void 0 : source;
|
|
16823
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
16824
|
+
const candidate = source;
|
|
16825
|
+
const id = candidate.header?.id ?? candidate.id;
|
|
16826
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
16827
|
+
}
|
|
16828
|
+
function installForkInheritance(ctx, deps) {
|
|
16829
|
+
const service = ctx.get("sessions");
|
|
16830
|
+
const original = service?.fork;
|
|
16831
|
+
if (typeof service !== "object" || service === null || typeof original !== "function") {
|
|
16832
|
+
return { installed: false, dispose: () => void 0 };
|
|
16833
|
+
}
|
|
16834
|
+
if (original[PATCHED] === true) {
|
|
16835
|
+
return { installed: true, dispose: () => void 0 };
|
|
16836
|
+
}
|
|
16837
|
+
const invoke = original;
|
|
16838
|
+
const patched2 = function patchedFork(...args) {
|
|
16839
|
+
const child = invoke.apply(this, args);
|
|
16840
|
+
try {
|
|
16841
|
+
const sourceId = sourceIdOf(args[0]);
|
|
16842
|
+
const childId = sourceIdOf(child);
|
|
16843
|
+
if (sourceId !== void 0 && childId !== void 0) {
|
|
16844
|
+
markForkInheritancePending(childId, sourceId);
|
|
16845
|
+
void deps.inherit(sourceId, childId).catch((error) => {
|
|
16846
|
+
deps.warn?.(`fork \u7EE7\u627F\u6DFB\u52A0\u76EE\u5F55\u5931\u8D25\uFF08source=${sourceId} child=${childId}\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16847
|
+
}).finally(() => {
|
|
16848
|
+
clearForkInheritancePending(childId);
|
|
16849
|
+
});
|
|
16850
|
+
}
|
|
16851
|
+
} catch (error) {
|
|
16852
|
+
deps.warn?.(`fork \u7EE7\u627F\u6DFB\u52A0\u76EE\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16853
|
+
}
|
|
16854
|
+
return child;
|
|
16855
|
+
};
|
|
16856
|
+
patched2[PATCHED] = true;
|
|
16857
|
+
service.fork = patched2;
|
|
16858
|
+
return {
|
|
16859
|
+
installed: true,
|
|
16860
|
+
dispose: () => {
|
|
16861
|
+
if (service.fork === patched2) {
|
|
16862
|
+
service.fork = original;
|
|
16863
|
+
}
|
|
16864
|
+
clearAllForkInheritancePending();
|
|
16865
|
+
}
|
|
16866
|
+
};
|
|
16867
|
+
}
|
|
16868
|
+
|
|
16444
16869
|
// src/wiring/fs-panel-routes.ts
|
|
16445
16870
|
function readOnlySeams(env) {
|
|
16446
16871
|
const { ctx, profiles, registry, transports } = env;
|
|
@@ -16658,6 +17083,8 @@ function registerPanelRoutes(env, hooks) {
|
|
|
16658
17083
|
stateDir: resolveStateDir(),
|
|
16659
17084
|
// Published in the GET snapshot so a client can recognise a session whose cwd is a remote workspace.
|
|
16660
17085
|
anchorRootDir: env.anchorRootDir,
|
|
17086
|
+
// fork 继承父会话添加的目录(ADR-0013):血缘只有宿主知道,所以 `GET /roots?session=` 由这里回答。
|
|
17087
|
+
resolveSessionAncestors: (sessionId) => readSessionAncestors(env.ctx, sessionId),
|
|
16661
17088
|
...readOnlySeams(env),
|
|
16662
17089
|
...rootLifecycleSeams(env, hooks),
|
|
16663
17090
|
...connectionLifecycleSeams(env, hooks),
|
|
@@ -16665,11 +17092,15 @@ function registerPanelRoutes(env, hooks) {
|
|
|
16665
17092
|
...remoteFileSeams(env),
|
|
16666
17093
|
webServerProvider: ctx
|
|
16667
17094
|
});
|
|
16668
|
-
|
|
17095
|
+
const forkInheritance = installForkInheritance(ctx, {
|
|
17096
|
+
inherit: (sourceSessionId, childSessionId) => rootsWiring.inheritAddedRoots(sourceSessionId, childSessionId),
|
|
17097
|
+
warn: (message) => ctx.logger?.warn?.(`over-ssh: ${message}`)
|
|
17098
|
+
});
|
|
17099
|
+
return { sessionWiring, rootsWiring, disposeForkInheritance: () => forkInheritance.dispose() };
|
|
16669
17100
|
}
|
|
16670
17101
|
|
|
16671
17102
|
// src/wiring/fs.ts
|
|
16672
|
-
var LOCAL_WORLD_MISSING_WARNING = 'dsh-over-ssh: \u79C1\u6709\u57DF\u7684 bridge \u672A\u5C31\u4F4D\uFF08ctx.get("localFs") === undefined\uFF09\uFF1A\u672C\u5730\u8DEF\u5F84\u7684\u5199\u5165\u56F4\u680F\u9000\u56DE\u672C\u63D2\u4EF6\u81EA\u5DF1\u7684 LocalBackend\u2014\u2014\u529F\u80FD\u770B\u8D77\u6765\u6B63\u5E38\uFF0C\u4F46\u56F4\u680F\u6362\u4E86\u6267\u884C\u8005\u3002\u8BF7\u68C0\u67E5 cordis.patch.yml \u91CC \u8FDC\u7A0B\u5DE5\u4F5C\u533A-\u79C1\u6709\u57DF \u7EC4\uFF08fs
|
|
17103
|
+
var LOCAL_WORLD_MISSING_WARNING = 'dsh-over-ssh: \u79C1\u6709\u57DF\u7684 bridge \u672A\u5C31\u4F4D\uFF08ctx.get("localFs") === undefined\uFF09\uFF1A\u672C\u5730\u8DEF\u5F84\u7684\u5199\u5165\u56F4\u680F\u9000\u56DE\u672C\u63D2\u4EF6\u81EA\u5DF1\u7684 LocalBackend\u2014\u2014\u529F\u80FD\u770B\u8D77\u6765\u6B63\u5E38\uFF0C\u4F46\u56F4\u680F\u6362\u4E86\u6267\u884C\u8005\u3002\u8BF7\u68C0\u67E5 cordis.patch.yml \u91CC \u8FDC\u7A0B\u5DE5\u4F5C\u533A-\u79C1\u6709\u57DF \u7EC4\uFF08fs \u7684 isolate \u4E0E \u8FDC\u7A0B\u5DE5\u4F5C\u533A-\u6865\u63A5fs \u884C\uFF09\u662F\u5426\u88AB\u7981\u7528\u6216\u52A0\u8F7D\u5931\u8D25\u3002';
|
|
16673
17104
|
function installClientDataChannel(ctx) {
|
|
16674
17105
|
try {
|
|
16675
17106
|
installRemoteFace(ctx);
|
|
@@ -16706,7 +17137,9 @@ function provideAgentStatus(env) {
|
|
|
16706
17137
|
}
|
|
16707
17138
|
};
|
|
16708
17139
|
const forgetPerConnection = (connectionId) => {
|
|
16709
|
-
void forgetPerConnectionAsync(connectionId)
|
|
17140
|
+
void forgetPerConnectionAsync(connectionId).catch((error) => {
|
|
17141
|
+
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)}`);
|
|
17142
|
+
});
|
|
16710
17143
|
};
|
|
16711
17144
|
const disposeAgent = ctx.provide("sshDevAgent", {
|
|
16712
17145
|
outcome: (connectionId) => agentBridge.installationFor(connectionId),
|
|
@@ -16739,7 +17172,9 @@ function registerConfiguredRoots(env) {
|
|
|
16739
17172
|
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
17173
|
});
|
|
16741
17174
|
}
|
|
16742
|
-
void Promise.allSettled(binRootPreparation).then(runBinHousekeeping)
|
|
17175
|
+
void Promise.allSettled(binRootPreparation).then(runBinHousekeeping).catch((error) => {
|
|
17176
|
+
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)}`);
|
|
17177
|
+
});
|
|
16743
17178
|
}
|
|
16744
17179
|
function scheduleActivationDiagnostics(env) {
|
|
16745
17180
|
const { ctx, rootsWiring, sessionWiring } = env;
|
|
@@ -16765,7 +17200,7 @@ function scheduleActivationDiagnostics(env) {
|
|
|
16765
17200
|
function watchSessionRemovals(env) {
|
|
16766
17201
|
const { ctx, rootsWiring, onSessionRemoved } = env;
|
|
16767
17202
|
onSessionRemoved((sessionId) => {
|
|
16768
|
-
void rootsWiring.
|
|
17203
|
+
void rootsWiring.pruneSession(sessionId).catch((error) => {
|
|
16769
17204
|
ctx.logger?.warn?.(
|
|
16770
17205
|
`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
17206
|
);
|
|
@@ -16784,6 +17219,7 @@ function createTeardown(env) {
|
|
|
16784
17219
|
const { activationTimers, routes, hostServices, disposeAgent, tools, disposeSessionDirectories } = env;
|
|
16785
17220
|
return async () => {
|
|
16786
17221
|
for (const timer of activationTimers) clearTimeout(timer);
|
|
17222
|
+
routes.disposeForkInheritance();
|
|
16787
17223
|
routes.rootsWiring.dispose();
|
|
16788
17224
|
routes.sessionWiring.dispose();
|
|
16789
17225
|
tools.disposeManagement();
|
|
@@ -16861,7 +17297,7 @@ import { dirname as dirname7, join as join16 } from "node:path";
|
|
|
16861
17297
|
// src/host/integration/transfer/transfer-adapters.ts
|
|
16862
17298
|
init_errors();
|
|
16863
17299
|
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
|
|
17300
|
+
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
17301
|
import { dirname as dirname6 } from "node:path";
|
|
16866
17302
|
function requireRemote(deps, path) {
|
|
16867
17303
|
const target = deps.routed.resolve(path);
|
|
@@ -16925,7 +17361,7 @@ function createTransferDeps(deps) {
|
|
|
16925
17361
|
},
|
|
16926
17362
|
async statLocal(path) {
|
|
16927
17363
|
try {
|
|
16928
|
-
const info = await
|
|
17364
|
+
const info = await stat6(path);
|
|
16929
17365
|
return toBackendStat(info, localVersion(info));
|
|
16930
17366
|
} catch {
|
|
16931
17367
|
return toBackendStat(void 0, "0:0");
|
|
@@ -17495,6 +17931,10 @@ function wireRoots(env) {
|
|
|
17495
17931
|
ctx.logger?.warn?.(
|
|
17496
17932
|
`\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5DF2\u8FC7\u671F\u88AB\u56DE\u6536\uFF1A\u6301\u6709\u8005 ${JSON.stringify(event.holder)}\uFF0C\u5DF2\u8FC7\u671F ${Math.round(event.ageMs / 1e3)} \u79D2`
|
|
17497
17933
|
);
|
|
17934
|
+
},
|
|
17935
|
+
// 心跳失败只警告:锁仍然有效,操作继续;但网络盘 EIO/EPERM 值得在日志里留下痕迹。
|
|
17936
|
+
onHeartbeatError: (error) => {
|
|
17937
|
+
ctx.logger?.warn?.(`\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5FC3\u8DF3\u5237\u65B0\u5931\u8D25\uFF08\u9501\u4ECD\u6301\u6709\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17498
17938
|
}
|
|
17499
17939
|
});
|
|
17500
17940
|
binLocks.set(key, created);
|
|
@@ -17676,7 +18116,7 @@ function createSessionRootIndex(deps) {
|
|
|
17676
18116
|
const now = deps.now ?? (() => Date.now());
|
|
17677
18117
|
let cachedAt;
|
|
17678
18118
|
let cached2 = [];
|
|
17679
|
-
let
|
|
18119
|
+
let pending2;
|
|
17680
18120
|
let generation = 0;
|
|
17681
18121
|
const readEntries = async () => {
|
|
17682
18122
|
try {
|
|
@@ -17690,14 +18130,14 @@ function createSessionRootIndex(deps) {
|
|
|
17690
18130
|
const entriesNow = () => {
|
|
17691
18131
|
const at = cachedAt;
|
|
17692
18132
|
const fresh = at !== void 0 && now() - at < deps.ttlMs;
|
|
17693
|
-
if (!fresh &&
|
|
18133
|
+
if (!fresh && pending2 === void 0) {
|
|
17694
18134
|
const startedAt = generation;
|
|
17695
|
-
|
|
18135
|
+
pending2 = readEntries().then((entries) => {
|
|
17696
18136
|
if (startedAt === generation) {
|
|
17697
18137
|
cached2 = entries;
|
|
17698
18138
|
cachedAt = now();
|
|
17699
18139
|
}
|
|
17700
|
-
|
|
18140
|
+
pending2 = void 0;
|
|
17701
18141
|
return entries;
|
|
17702
18142
|
});
|
|
17703
18143
|
}
|
|
@@ -17712,7 +18152,7 @@ function createSessionRootIndex(deps) {
|
|
|
17712
18152
|
generation += 1;
|
|
17713
18153
|
cachedAt = void 0;
|
|
17714
18154
|
cached2 = [];
|
|
17715
|
-
|
|
18155
|
+
pending2 = void 0;
|
|
17716
18156
|
}
|
|
17717
18157
|
};
|
|
17718
18158
|
}
|
|
@@ -17771,8 +18211,12 @@ function resolveSessionTarget(exec, seams) {
|
|
|
17771
18211
|
function readConnectionIds(record2) {
|
|
17772
18212
|
const raw = record2["connectionIds"];
|
|
17773
18213
|
if (!Array.isArray(raw)) return void 0;
|
|
18214
|
+
if (raw.length > MAX_EXEC_BATCH_CONNECTIONS) {
|
|
18215
|
+
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`);
|
|
18216
|
+
}
|
|
17774
18217
|
const ids = [...new Set(raw.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()))];
|
|
17775
|
-
|
|
18218
|
+
if (ids.length === 0) return void 0;
|
|
18219
|
+
return ids;
|
|
17776
18220
|
}
|
|
17777
18221
|
function text2(value) {
|
|
17778
18222
|
return [{ type: "text", text: value }];
|
|
@@ -18239,6 +18683,7 @@ var DEFAULT_JOB_POLL_MS = 500;
|
|
|
18239
18683
|
var WAIT_SLICE_MS = 5e3;
|
|
18240
18684
|
var DEFAULT_JOB_OUTPUT_BYTES = 1 << 20;
|
|
18241
18685
|
var MAX_JOB_OUTPUT_BYTES = 8 << 20;
|
|
18686
|
+
var MAX_JOB_STDIN_BYTES = 1 << 20;
|
|
18242
18687
|
function isAgentJobCapable(transport) {
|
|
18243
18688
|
if (transport === null || typeof transport !== "object") return false;
|
|
18244
18689
|
const candidate = transport;
|
|
@@ -18598,6 +19043,11 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18598
19043
|
const maxOutputBytes = clampInt2(record2["maxOutputBytes"], DEFAULT_JOB_OUTPUT_BYTES, 1, MAX_JOB_OUTPUT_BYTES);
|
|
18599
19044
|
const stdinText = record2["stdin"];
|
|
18600
19045
|
if (stdinText !== void 0 && typeof stdinText !== "string") throw new Error("stdin \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
19046
|
+
if (typeof stdinText === "string" && Buffer.byteLength(stdinText, "utf8") > MAX_JOB_STDIN_BYTES) {
|
|
19047
|
+
throw new Error(
|
|
19048
|
+
`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`
|
|
19049
|
+
);
|
|
19050
|
+
}
|
|
18601
19051
|
const interactive = record2["interactive"] === true;
|
|
18602
19052
|
const target = targetFor2(backend, exec);
|
|
18603
19053
|
const cwd = resolveOrThrow2(target, stringArg2(record2["cwd"]));
|
|
@@ -18717,6 +19167,9 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18717
19167
|
};
|
|
18718
19168
|
}
|
|
18719
19169
|
if (action === "stdin") {
|
|
19170
|
+
if (typeof record2["data"] === "string" && typeof record2["dataBase64"] === "string") {
|
|
19171
|
+
throw new Error("data \u4E0E dataBase64 \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
19172
|
+
}
|
|
18720
19173
|
const result = await backend.stdin(
|
|
18721
19174
|
target,
|
|
18722
19175
|
jobId,
|
|
@@ -18935,16 +19388,16 @@ function createRemoteFileBackend(deps) {
|
|
|
18935
19388
|
if (carried !== void 0) rememberVersion(target, to, carried);
|
|
18936
19389
|
},
|
|
18937
19390
|
async stat(target, absolute, signal) {
|
|
18938
|
-
const
|
|
18939
|
-
if (
|
|
19391
|
+
const stat7 = await requireBackend(target).stat(absolute, signal);
|
|
19392
|
+
if (stat7.exists) rememberVersion(target, absolute, stat7.version);
|
|
18940
19393
|
return {
|
|
18941
|
-
exists:
|
|
18942
|
-
isDirectory:
|
|
18943
|
-
isFile:
|
|
18944
|
-
isSymlink:
|
|
18945
|
-
size:
|
|
18946
|
-
mtimeMs:
|
|
18947
|
-
version:
|
|
19394
|
+
exists: stat7.exists,
|
|
19395
|
+
isDirectory: stat7.isDirectory,
|
|
19396
|
+
isFile: stat7.isFile,
|
|
19397
|
+
isSymlink: stat7.isSymlink,
|
|
19398
|
+
size: stat7.size,
|
|
19399
|
+
mtimeMs: stat7.mtimeMs,
|
|
19400
|
+
version: stat7.version
|
|
18948
19401
|
};
|
|
18949
19402
|
},
|
|
18950
19403
|
async patch(target, absolute, diff, expectedVersion, signal) {
|
|
@@ -19400,6 +19853,9 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19400
19853
|
const from = resolveOrThrow3(target, rawFrom);
|
|
19401
19854
|
const to = resolveOrThrow3(target, rawTo);
|
|
19402
19855
|
if (from === to) throw new Error(`\u6E90\u4E0E\u76EE\u6807\u662F\u540C\u4E00\u4E2A\u8DEF\u5F84\uFF1A${from}`);
|
|
19856
|
+
if (to.startsWith(from.endsWith("/") ? from : `${from}/`)) {
|
|
19857
|
+
throw new Error(`\u4E0D\u80FD\u628A\u76EE\u5F55\u79FB\u52A8\u5230\u5B83\u81EA\u5DF1\u7684\u5B50\u76EE\u5F55\u91CC\uFF1A${from} \u2192 ${to}`);
|
|
19858
|
+
}
|
|
19403
19859
|
assertWritable(target, to, allowMutation);
|
|
19404
19860
|
const createParents = record2["createParents"] !== false;
|
|
19405
19861
|
await backend.move(target, from, to, createParents, exec.signal);
|
|
@@ -19436,15 +19892,15 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
19436
19892
|
const requested = requiredString3(record2["path"], "path");
|
|
19437
19893
|
const target = targetFor3(backend, exec);
|
|
19438
19894
|
const path = resolveOrThrow3(target, requested);
|
|
19439
|
-
const
|
|
19440
|
-
const kind = !
|
|
19895
|
+
const stat7 = await backend.stat(target, path, exec.signal);
|
|
19896
|
+
const kind = !stat7.exists ? "missing" : stat7.isSymlink ? "symlink" : stat7.isDirectory ? "directory" : "file";
|
|
19441
19897
|
return {
|
|
19442
19898
|
path,
|
|
19443
|
-
exists:
|
|
19899
|
+
exists: stat7.exists,
|
|
19444
19900
|
kind,
|
|
19445
|
-
size:
|
|
19446
|
-
mtimeMs:
|
|
19447
|
-
version:
|
|
19901
|
+
size: stat7.exists ? stat7.size : 0,
|
|
19902
|
+
mtimeMs: stat7.exists ? stat7.mtimeMs : 0,
|
|
19903
|
+
version: stat7.exists ? stat7.version : ""
|
|
19448
19904
|
};
|
|
19449
19905
|
}
|
|
19450
19906
|
})
|
|
@@ -19524,11 +19980,11 @@ var LspFrameParser = class {
|
|
|
19524
19980
|
#append(bytes) {
|
|
19525
19981
|
if (bytes.length === 0) return;
|
|
19526
19982
|
if (this.#end + bytes.length > this.#store.length) {
|
|
19527
|
-
const
|
|
19983
|
+
const pending2 = this.#end - this.#start;
|
|
19528
19984
|
if (this.#start > 0) {
|
|
19529
19985
|
this.#store.copyWithin(0, this.#start, this.#end);
|
|
19530
19986
|
this.#start = 0;
|
|
19531
|
-
this.#end =
|
|
19987
|
+
this.#end = pending2;
|
|
19532
19988
|
}
|
|
19533
19989
|
if (this.#end + bytes.length > this.#store.length) {
|
|
19534
19990
|
let capacity = Math.max(this.#store.length, 64 * 1024);
|
|
@@ -19537,7 +19993,7 @@ var LspFrameParser = class {
|
|
|
19537
19993
|
this.#store.copy(next, 0, this.#start, this.#end);
|
|
19538
19994
|
this.#store = next;
|
|
19539
19995
|
this.#start = 0;
|
|
19540
|
-
this.#end =
|
|
19996
|
+
this.#end = pending2;
|
|
19541
19997
|
}
|
|
19542
19998
|
}
|
|
19543
19999
|
this.#store.set(bytes, this.#end);
|
|
@@ -19560,6 +20016,7 @@ function parseContentLength(header) {
|
|
|
19560
20016
|
}
|
|
19561
20017
|
|
|
19562
20018
|
// src/host/integration/lsp/lsp-session.ts
|
|
20019
|
+
var LSP_READ_CHUNK_BYTES = 256 * 1024;
|
|
19563
20020
|
var IDLE_POLL_MS = 60;
|
|
19564
20021
|
var BUSY_POLL_MS = 0;
|
|
19565
20022
|
var DEFAULT_REQUEST_TIMEOUT_MS = 2e4;
|
|
@@ -19749,11 +20206,11 @@ var LspSession = class _LspSession {
|
|
|
19749
20206
|
throw error;
|
|
19750
20207
|
}
|
|
19751
20208
|
const onAbort = () => {
|
|
19752
|
-
const
|
|
19753
|
-
if (
|
|
20209
|
+
const pending2 = this.#pending.get(id);
|
|
20210
|
+
if (pending2 === void 0) return;
|
|
19754
20211
|
this.#pending.delete(id);
|
|
19755
|
-
clearTimeout(
|
|
19756
|
-
|
|
20212
|
+
clearTimeout(pending2.timer);
|
|
20213
|
+
pending2.reject(new LspSessionError("\u7B49\u5F85\u8BED\u8A00\u670D\u52A1\u5668\u65F6\u88AB\u53D6\u6D88"));
|
|
19757
20214
|
};
|
|
19758
20215
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
19759
20216
|
try {
|
|
@@ -19848,7 +20305,7 @@ var LspSession = class _LspSession {
|
|
|
19848
20305
|
if (this.#stopped) return;
|
|
19849
20306
|
let delay = IDLE_POLL_MS;
|
|
19850
20307
|
try {
|
|
19851
|
-
const chunk = await this.#transport.lspRead(this.#id, this.#offset, signal);
|
|
20308
|
+
const chunk = await this.#transport.lspRead(this.#id, this.#offset, signal, LSP_READ_CHUNK_BYTES);
|
|
19852
20309
|
this.#offset = chunk.nextOffset;
|
|
19853
20310
|
if (chunk.lost) {
|
|
19854
20311
|
this.#failAll(new LspSessionError("\u8BED\u8A00\u670D\u52A1\u5668\u8F93\u51FA\u5DF2\u88AB\u4E22\u5F03\uFF08\u73AF\u5F62\u7F13\u51B2\u6EA2\u51FA\uFF09\uFF0C\u6D41\u4E0D\u53EF\u6062\u590D"));
|
|
@@ -19877,15 +20334,15 @@ var LspSession = class _LspSession {
|
|
|
19877
20334
|
if (typeof message["method"] !== "string") {
|
|
19878
20335
|
const id = message["id"];
|
|
19879
20336
|
if (typeof id === "number") {
|
|
19880
|
-
const
|
|
19881
|
-
if (
|
|
20337
|
+
const pending2 = this.#pending.get(id);
|
|
20338
|
+
if (pending2 === void 0) return;
|
|
19882
20339
|
this.#pending.delete(id);
|
|
19883
|
-
clearTimeout(
|
|
20340
|
+
clearTimeout(pending2.timer);
|
|
19884
20341
|
const error = message["error"];
|
|
19885
20342
|
if (error !== void 0 && error !== null) {
|
|
19886
|
-
|
|
20343
|
+
pending2.reject(new LspSessionError(`${pending2.method} \u5931\u8D25\uFF1A${describeError(error)}`));
|
|
19887
20344
|
} else {
|
|
19888
|
-
|
|
20345
|
+
pending2.resolve(message["result"]);
|
|
19889
20346
|
}
|
|
19890
20347
|
}
|
|
19891
20348
|
return;
|
|
@@ -19941,10 +20398,10 @@ var LspSession = class _LspSession {
|
|
|
19941
20398
|
}
|
|
19942
20399
|
#failAll(error) {
|
|
19943
20400
|
this.#exitReason ??= error;
|
|
19944
|
-
for (const [id,
|
|
19945
|
-
clearTimeout(
|
|
20401
|
+
for (const [id, pending2] of this.#pending) {
|
|
20402
|
+
clearTimeout(pending2.timer);
|
|
19946
20403
|
this.#pending.delete(id);
|
|
19947
|
-
|
|
20404
|
+
pending2.reject(error);
|
|
19948
20405
|
}
|
|
19949
20406
|
}
|
|
19950
20407
|
};
|
|
@@ -20108,8 +20565,12 @@ function describeInstalled(available) {
|
|
|
20108
20565
|
return `\u8BE5\u4E3B\u673A\u4E0A\u63A2\u6D4B\u5230\u7684\u8BED\u8A00\u670D\u52A1\u5668\uFF1A${list}\u3002`;
|
|
20109
20566
|
}
|
|
20110
20567
|
var MAX_SESSIONS = 8;
|
|
20111
|
-
function
|
|
20112
|
-
return `${target.connectionId}\0${target.root}\0${language}`;
|
|
20568
|
+
function sessionKeyPrefix(target, language) {
|
|
20569
|
+
return `${target.connectionId}\0${target.root}\0${language}\0`;
|
|
20570
|
+
}
|
|
20571
|
+
function sessionKey(target, language, override) {
|
|
20572
|
+
const server = override === void 0 ? "" : `${override.command}\0${(override.args ?? []).join("\0")}`;
|
|
20573
|
+
return `${sessionKeyPrefix(target, language)}${server}`;
|
|
20113
20574
|
}
|
|
20114
20575
|
function createLspRegistry(deps) {
|
|
20115
20576
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -20128,15 +20589,15 @@ function createLspRegistry(deps) {
|
|
|
20128
20589
|
}
|
|
20129
20590
|
return {
|
|
20130
20591
|
async session(target, language, override, signal) {
|
|
20131
|
-
const key = sessionKey(target, language);
|
|
20592
|
+
const key = sessionKey(target, language, override);
|
|
20132
20593
|
const existing = sessions.get(key);
|
|
20133
20594
|
if (existing !== void 0 && !existing.session.stopped) {
|
|
20134
20595
|
sessions.delete(key);
|
|
20135
20596
|
sessions.set(key, existing);
|
|
20136
20597
|
return existing.session;
|
|
20137
20598
|
}
|
|
20138
|
-
const
|
|
20139
|
-
if (
|
|
20599
|
+
const pending2 = inFlight.get(key);
|
|
20600
|
+
if (pending2 !== void 0) return pending2;
|
|
20140
20601
|
const attempt = (async () => {
|
|
20141
20602
|
const command = override ?? defaultServerFor(language);
|
|
20142
20603
|
if (command === void 0) {
|
|
@@ -20208,17 +20669,21 @@ ${describeInstalled(available)}`);
|
|
|
20208
20669
|
}));
|
|
20209
20670
|
},
|
|
20210
20671
|
async stop(target, language, signal) {
|
|
20211
|
-
const
|
|
20212
|
-
const
|
|
20213
|
-
if (
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20221
|
-
|
|
20672
|
+
const prefix = sessionKeyPrefix(target, language);
|
|
20673
|
+
const keys = [...new Set([...sessions.keys(), ...inFlight.keys()].filter((key) => key.startsWith(prefix)))];
|
|
20674
|
+
if (keys.length === 0) return false;
|
|
20675
|
+
for (const key of keys) {
|
|
20676
|
+
const starting = inFlight.get(key);
|
|
20677
|
+
if (starting !== void 0) {
|
|
20678
|
+
retired.add(key);
|
|
20679
|
+
await starting.catch(() => void 0);
|
|
20680
|
+
retired.delete(key);
|
|
20681
|
+
}
|
|
20682
|
+
const entry = sessions.get(key);
|
|
20683
|
+
if (entry === void 0) continue;
|
|
20684
|
+
sessions.delete(key);
|
|
20685
|
+
await entry.session.close(signal);
|
|
20686
|
+
}
|
|
20222
20687
|
return true;
|
|
20223
20688
|
},
|
|
20224
20689
|
async disposeAll() {
|
|
@@ -20802,9 +21267,44 @@ function readArgs(args) {
|
|
|
20802
21267
|
...typeof record2["server"] === "string" && record2["server"] !== "" ? { server: record2["server"] } : {}
|
|
20803
21268
|
};
|
|
20804
21269
|
}
|
|
21270
|
+
function splitCommandLine(value) {
|
|
21271
|
+
const parts = [];
|
|
21272
|
+
let current = "";
|
|
21273
|
+
let started = false;
|
|
21274
|
+
let quote2;
|
|
21275
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
21276
|
+
const char = value[index];
|
|
21277
|
+
if (quote2 !== void 0) {
|
|
21278
|
+
if (char === quote2) quote2 = void 0;
|
|
21279
|
+
else if (char === "\\" && quote2 === '"' && index + 1 < value.length) {
|
|
21280
|
+
current += value[index + 1];
|
|
21281
|
+
index += 1;
|
|
21282
|
+
} else current += char;
|
|
21283
|
+
continue;
|
|
21284
|
+
}
|
|
21285
|
+
if (char === '"' || char === "'") {
|
|
21286
|
+
quote2 = char;
|
|
21287
|
+
started = true;
|
|
21288
|
+
continue;
|
|
21289
|
+
}
|
|
21290
|
+
if (/\s/.test(char)) {
|
|
21291
|
+
if (started) {
|
|
21292
|
+
parts.push(current);
|
|
21293
|
+
current = "";
|
|
21294
|
+
started = false;
|
|
21295
|
+
}
|
|
21296
|
+
continue;
|
|
21297
|
+
}
|
|
21298
|
+
current += char;
|
|
21299
|
+
started = true;
|
|
21300
|
+
}
|
|
21301
|
+
if (quote2 !== void 0) throw new Error('server \u7684\u5F15\u53F7\u6CA1\u6709\u95ED\u5408\uFF1B\u5E26\u7A7A\u683C\u7684\u8DEF\u5F84\u8BF7\u5199\u6210 "/opt/my tools/server" --stdio');
|
|
21302
|
+
if (started) parts.push(current);
|
|
21303
|
+
return parts;
|
|
21304
|
+
}
|
|
20805
21305
|
function parseServer(value) {
|
|
20806
21306
|
if (value === void 0) return void 0;
|
|
20807
|
-
const parts = value
|
|
21307
|
+
const parts = splitCommandLine(value);
|
|
20808
21308
|
if (parts.length === 0) return void 0;
|
|
20809
21309
|
return parts.length === 1 ? { command: parts[0] } : { command: parts[0], args: parts.slice(1) };
|
|
20810
21310
|
}
|
|
@@ -20826,7 +21326,7 @@ function remoteLspToolDefinitions(deps) {
|
|
|
20826
21326
|
column: { type: "integer", description: "Column for `hover` / `definition`, 1-based. Required for those two actions." },
|
|
20827
21327
|
server: {
|
|
20828
21328
|
type: "string",
|
|
20829
|
-
description:
|
|
21329
|
+
description: 'Executable to run, with optional arguments (`gopls`, `rust-analyzer`, `pylsp -v`). Defaults to the table entry for the file language; required when the language has no entry. Quote a path that contains spaces: `"/opt/my tools/server" --stdio`.'
|
|
20830
21330
|
}
|
|
20831
21331
|
},
|
|
20832
21332
|
output: {
|
|
@@ -21020,7 +21520,6 @@ function registerRemoteLspTools(ctx, deps) {
|
|
|
21020
21520
|
// src/wiring/tools-session.ts
|
|
21021
21521
|
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
21522
|
import { dirname as dirname8, join as join17 } from "node:path";
|
|
21023
|
-
import { homedir as homedir6 } from "node:os";
|
|
21024
21523
|
|
|
21025
21524
|
// src/host/integration/session/session-labels.ts
|
|
21026
21525
|
function normalizePath(path) {
|
|
@@ -21070,8 +21569,8 @@ function createWorkspaceTitleResolver(ctx) {
|
|
|
21070
21569
|
// src/wiring/tools-session.ts
|
|
21071
21570
|
function wireSessionAdmin(env) {
|
|
21072
21571
|
const { ctx, limits, notifySessionRemoved: notifySessionRemoved2 } = env;
|
|
21073
|
-
const sessionsRoot = limits.sessionsRoot ?? join17(
|
|
21074
|
-
const storagesRoot = join17(
|
|
21572
|
+
const sessionsRoot = limits.sessionsRoot ?? join17(dshHome(), "sessions");
|
|
21573
|
+
const storagesRoot = join17(dshHome(), "storages");
|
|
21075
21574
|
const sessionBinRoot = limits.sessionBinRoot ?? join17(storagesRoot, "over-ssh", "session-trash");
|
|
21076
21575
|
if (limits.sessionBinRoot === void 0) {
|
|
21077
21576
|
const carried = carryDirectory(join17(storagesRoot, "remote-workspace", "session-trash"), sessionBinRoot);
|
|
@@ -21086,6 +21585,10 @@ function wireSessionAdmin(env) {
|
|
|
21086
21585
|
ctx.logger?.warn?.(
|
|
21087
21586
|
`\u4F1A\u8BDD\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5DF2\u8FC7\u671F\u88AB\u56DE\u6536\uFF1A\u6301\u6709\u8005 ${JSON.stringify(event.holder)}\uFF0C\u5DF2\u8FC7\u671F ${Math.round(event.ageMs / 1e3)} \u79D2`
|
|
21088
21587
|
);
|
|
21588
|
+
},
|
|
21589
|
+
// 心跳失败只警告:锁仍有效,操作继续;网络盘 EIO/EPERM 需要留下痕迹。
|
|
21590
|
+
onHeartbeatError: (error) => {
|
|
21591
|
+
ctx.logger?.warn?.(`\u4F1A\u8BDD\u56DE\u6536\u7AD9\u7D22\u5F15\u9501\u5FC3\u8DF3\u5237\u65B0\u5931\u8D25\uFF08\u9501\u4ECD\u6301\u6709\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
21089
21592
|
}
|
|
21090
21593
|
});
|
|
21091
21594
|
const sessionAdmin = createSessionAdminService({
|
|
@@ -21122,15 +21625,19 @@ function wireSessionAdmin(env) {
|
|
|
21122
21625
|
throw error;
|
|
21123
21626
|
}
|
|
21124
21627
|
},
|
|
21125
|
-
|
|
21628
|
+
// The listing is already in hand: hand it over so the header cache can key on it instead of re-listing.
|
|
21629
|
+
readHeader: (dir, listing) => readSessionHeader(
|
|
21630
|
+
dir,
|
|
21631
|
+
listing?.map((entry) => ({ name: entry.name, size: entry.bytes, mtimeMs: entry.mtimeMs }))
|
|
21632
|
+
),
|
|
21126
21633
|
// Both resolvers are optional: a profile without the platform services still lists sessions, labelled
|
|
21127
21634
|
// by their directory and id rather than by name.
|
|
21128
21635
|
resolveSessionTitle: createSessionTitleResolver(ctx),
|
|
21129
21636
|
resolveWorkspaceTitle: createWorkspaceTitleResolver(ctx),
|
|
21130
21637
|
stat: async (path) => {
|
|
21131
21638
|
try {
|
|
21132
|
-
const
|
|
21133
|
-
return { isDirectory:
|
|
21639
|
+
const stat7 = await nodeStat2(path);
|
|
21640
|
+
return { isDirectory: stat7.isDirectory(), bytes: stat7.size };
|
|
21134
21641
|
} catch {
|
|
21135
21642
|
return void 0;
|
|
21136
21643
|
}
|
|
@@ -21371,6 +21878,7 @@ function managementToolDefinitions(backend) {
|
|
|
21371
21878
|
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
21879
|
}
|
|
21373
21880
|
const target = targetFor4(backend, exec);
|
|
21881
|
+
if (direction === "upload") backend.assertWritable?.(target, "\u4E0A\u4F20\u8FDC\u7AEF\u6587\u4EF6");
|
|
21374
21882
|
if (direction === "upload" && backend.statRemote !== void 0 && (expectedVersion !== void 0 || !force && expectedSha256 === void 0)) {
|
|
21375
21883
|
const absolute = resolveUnderRoot(target.root, remotePath);
|
|
21376
21884
|
if (typeof absolute !== "string") throw new Error(absolute.error);
|
|
@@ -21458,6 +21966,7 @@ function managementToolDefinitions(backend) {
|
|
|
21458
21966
|
const target = targetFor4(backend, exec);
|
|
21459
21967
|
const resolved = resolveUnderRoot(target.root, requested, void 0, target.connectionId);
|
|
21460
21968
|
if (typeof resolved !== "string") throw new Error(resolved.error);
|
|
21969
|
+
backend.assertWritable?.(target, mode === "permanent" ? "\u6C38\u4E45\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6" : "\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6");
|
|
21461
21970
|
const result = await backend.discard({ target, path: resolved, mode, signal: exec.signal });
|
|
21462
21971
|
return {
|
|
21463
21972
|
mode: result.mode,
|
|
@@ -21517,6 +22026,8 @@ function managementToolDefinitions(backend) {
|
|
|
21517
22026
|
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
22027
|
}
|
|
21519
22028
|
const target = targetFor4(backend, exec);
|
|
22029
|
+
const listing = record2["list"] === true;
|
|
22030
|
+
if (!listing) backend.assertWritable?.(target, "\u4ECE\u56DE\u6536\u7AD9\u6062\u590D\u8FDC\u7AEF\u6587\u4EF6");
|
|
21520
22031
|
const outcome = await backend.restore({
|
|
21521
22032
|
target,
|
|
21522
22033
|
id: typeof id === "string" ? id : void 0,
|
|
@@ -21573,6 +22084,7 @@ function managementToolDefinitions(backend) {
|
|
|
21573
22084
|
if (typeof source !== "string") throw new Error(source.error);
|
|
21574
22085
|
const destination = resolveUnderRoot(target.root, to, void 0, target.connectionId);
|
|
21575
22086
|
if (typeof destination !== "string") throw new Error(destination.error);
|
|
22087
|
+
backend.assertWritable?.(target, "\u590D\u5236\u8FDC\u7AEF\u6587\u4EF6");
|
|
21576
22088
|
const outcome = await backend.copy({
|
|
21577
22089
|
target,
|
|
21578
22090
|
from: source,
|
|
@@ -21807,6 +22319,8 @@ function wireManagementTools(env) {
|
|
|
21807
22319
|
)?.root;
|
|
21808
22320
|
const managementBackend = {
|
|
21809
22321
|
resolve: (cwd) => toolBackend.resolve(cwd),
|
|
22322
|
+
// 管理工具(上传/删除/恢复/复制)之前完全不过写闸:read-only 会话照样能删文件(审计 2026-11-06 宿主 P0)。
|
|
22323
|
+
assertWritable: (target, action) => env.assertWritable(target, action),
|
|
21810
22324
|
// ADR-0013: the session id (plus its fork lineage) is the strict scope; `cwd` alone cannot separate
|
|
21811
22325
|
// two conversations opened on the same directory.
|
|
21812
22326
|
resolveSessionRoot: (cwd, sessionId) => sessionRoots.outcomeFor(cwd, sessionIdentityOf(ctx, sessionId)),
|
|
@@ -21926,6 +22440,7 @@ function wireManagementTools(env) {
|
|
|
21926
22440
|
}
|
|
21927
22441
|
|
|
21928
22442
|
// src/wiring/tools.ts
|
|
22443
|
+
init_errors();
|
|
21929
22444
|
function sessionRootsFilePath() {
|
|
21930
22445
|
return join18(resolveStateDir(), USER_ROOTS_FILE_NAME);
|
|
21931
22446
|
}
|
|
@@ -21966,7 +22481,8 @@ function wireTools(env) {
|
|
|
21966
22481
|
disposeJobTools,
|
|
21967
22482
|
disposeFileTools,
|
|
21968
22483
|
lspRegistry,
|
|
21969
|
-
disposeLspTools
|
|
22484
|
+
disposeLspTools,
|
|
22485
|
+
assertWritable: assertWritable2
|
|
21970
22486
|
} = wireFileAndJobTools({ ctx, limits, profiles, transports, registry, sessionRoots });
|
|
21971
22487
|
const sessionAdmin = wireSessionAdmin({ ctx, limits, notifySessionRemoved: notifySessionRemoved2 });
|
|
21972
22488
|
const { dispose: disposeManagement } = wireManagementTools({
|
|
@@ -21980,7 +22496,8 @@ function wireTools(env) {
|
|
|
21980
22496
|
sessionRoots,
|
|
21981
22497
|
recordFor,
|
|
21982
22498
|
transferService,
|
|
21983
|
-
transferTracker
|
|
22499
|
+
transferTracker,
|
|
22500
|
+
assertWritable: assertWritable2
|
|
21984
22501
|
});
|
|
21985
22502
|
const disposeStrictArguments = strictArgumentsGate(ctx);
|
|
21986
22503
|
return {
|
|
@@ -22049,7 +22566,9 @@ function wireAgentTools(env) {
|
|
|
22049
22566
|
disposeRemoteTools();
|
|
22050
22567
|
disposeGitTool();
|
|
22051
22568
|
disposeForwardTool();
|
|
22052
|
-
void forwardBackend.closeAll()
|
|
22569
|
+
void forwardBackend.closeAll().catch((error) => {
|
|
22570
|
+
ctx.logger?.warn?.(`over-ssh: \u5173\u95ED\u7AEF\u53E3\u8F6C\u53D1\u65F6\u51FA\u9519\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
22571
|
+
});
|
|
22053
22572
|
};
|
|
22054
22573
|
return { sessionRoots, toolBackend, disposeTools };
|
|
22055
22574
|
}
|
|
@@ -22086,21 +22605,33 @@ function wireFileAndJobTools(env) {
|
|
|
22086
22605
|
backendFor: (target) => recordFor(target)?.backend,
|
|
22087
22606
|
...limits.maxReadBytes === void 0 ? {} : { maxReadBytes: limits.maxReadBytes }
|
|
22088
22607
|
});
|
|
22089
|
-
const allowMutation = (target) => {
|
|
22608
|
+
const allowMutation = (target, action) => {
|
|
22609
|
+
const policy = platformMutationPolicy(ctx);
|
|
22610
|
+
const sessionId = currentSessionId();
|
|
22611
|
+
const cwd = sessionId === void 0 || sessionId === "" ? void 0 : readSessionCwd(ctx, sessionId);
|
|
22090
22612
|
const refusal = mutationRefusal(
|
|
22091
|
-
`${target.connectionId}:${target.root}`,
|
|
22613
|
+
action === void 0 ? `${target.connectionId}:${target.root}` : `${action}\uFF1A${target.connectionId}:${target.root}`,
|
|
22092
22614
|
{
|
|
22093
|
-
...
|
|
22615
|
+
...policy,
|
|
22094
22616
|
rootArea: () => {
|
|
22095
22617
|
const record2 = recordFor(target);
|
|
22096
22618
|
if (record2 === void 0) return "deny";
|
|
22097
|
-
return workspaceWriteArea(record2.root,
|
|
22619
|
+
return workspaceWriteArea(record2.root, policy.policy?.workspaceRoot, cwd, sessionIdentityOf(ctx, sessionId));
|
|
22098
22620
|
}
|
|
22099
22621
|
}
|
|
22100
22622
|
);
|
|
22101
22623
|
if (refusal !== void 0) throw new Error(refusal);
|
|
22102
22624
|
};
|
|
22103
22625
|
const disposeFileTools = registerRemoteFileTools(ctx, fileBackend, { allowMutation });
|
|
22626
|
+
const assertWritable2 = (target, action) => {
|
|
22627
|
+
allowMutation(target, action);
|
|
22628
|
+
if (recordFor(target)?.root.writable === false) {
|
|
22629
|
+
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}`, {
|
|
22630
|
+
connectionId: target.connectionId,
|
|
22631
|
+
root: target.root
|
|
22632
|
+
});
|
|
22633
|
+
}
|
|
22634
|
+
};
|
|
22104
22635
|
const lspRegistry = createLspRegistry({
|
|
22105
22636
|
acquire: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport,
|
|
22106
22637
|
onLog: (connectionId, message) => {
|
|
@@ -22108,7 +22639,7 @@ function wireFileAndJobTools(env) {
|
|
|
22108
22639
|
}
|
|
22109
22640
|
});
|
|
22110
22641
|
const disposeLspTools = registerRemoteLspTools(ctx, { backend: fileBackend, registry: lspRegistry });
|
|
22111
|
-
return { recordFor, disposeJobTools, disposeFileTools, lspRegistry, disposeLspTools };
|
|
22642
|
+
return { recordFor, disposeJobTools, disposeFileTools, lspRegistry, disposeLspTools, assertWritable: assertWritable2 };
|
|
22112
22643
|
}
|
|
22113
22644
|
|
|
22114
22645
|
// src/index.ts
|