dsh-over-ssh 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/agents/agent-manifest.json +6 -6
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +613 -257
- package/lib/file-references.js +17 -0
- package/lib/fs.js +90 -20
- package/lib/index.js +1385 -463
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +11 -2
- package/lib/types/src/host/domain/session/session-admin.d.ts +5 -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/wiring/helpers.d.ts +11 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -420,7 +420,7 @@ function createProxyForwarder(options) {
|
|
|
420
420
|
}
|
|
421
421
|
const idleMs = now() - current.lastActivityAt;
|
|
422
422
|
if (idleMs < idleTimeoutMs) return;
|
|
423
|
-
void closeEntry(current).then(() => options.onReclaimed?.(handle, idleMs));
|
|
423
|
+
void closeEntry(current).then(() => options.onReclaimed?.(handle, idleMs)).catch(() => void 0);
|
|
424
424
|
}, checkMs);
|
|
425
425
|
entry.timer.unref?.();
|
|
426
426
|
}
|
|
@@ -1267,6 +1267,13 @@ ${buildRemoteScript(request.script)}`;
|
|
|
1267
1267
|
...request.env === void 0 ? {} : { env: request.env }
|
|
1268
1268
|
});
|
|
1269
1269
|
}
|
|
1270
|
+
/** Mark the transport lost **only if the dead session is still the current one** (a reconnect may have won). */
|
|
1271
|
+
#markSessionLost(session) {
|
|
1272
|
+
if (this.#session !== session) return;
|
|
1273
|
+
this.#sftp = void 0;
|
|
1274
|
+
this.#sftpPending = void 0;
|
|
1275
|
+
this.markState("lost");
|
|
1276
|
+
}
|
|
1270
1277
|
#requireSession() {
|
|
1271
1278
|
if (this.#session === void 0) {
|
|
1272
1279
|
throw new SshDevError("CONNECTION_LOST", "SSH \u4F1A\u8BDD\u5C1A\u672A\u5EFA\u7ACB", { connectionId: this.id });
|
|
@@ -1324,8 +1331,13 @@ ${buildRemoteScript(request.script)}`;
|
|
|
1324
1331
|
}
|
|
1325
1332
|
const connector = this.#options.connector ?? defaultConnector;
|
|
1326
1333
|
try {
|
|
1327
|
-
|
|
1334
|
+
const session = await connector(signal === void 0 ? connect2 : { ...connect2, signal });
|
|
1335
|
+
this.#session = session;
|
|
1328
1336
|
this.#sessionCount++;
|
|
1337
|
+
const onLost = () => this.#markSessionLost(session);
|
|
1338
|
+
session.on?.("close", onLost);
|
|
1339
|
+
session.on?.("error", onLost);
|
|
1340
|
+
session.on?.("end", onLost);
|
|
1329
1341
|
this.markState("ready");
|
|
1330
1342
|
} catch (error) {
|
|
1331
1343
|
this.markState("lost");
|
|
@@ -1651,6 +1663,35 @@ var TransportService = class {
|
|
|
1651
1663
|
|
|
1652
1664
|
// src/host/integration/session-scope.ts
|
|
1653
1665
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1666
|
+
|
|
1667
|
+
// src/host/integration/session-fork-pending.ts
|
|
1668
|
+
var pending = /* @__PURE__ */ new Map();
|
|
1669
|
+
var MAX_PENDING = 256;
|
|
1670
|
+
function markForkInheritancePending(childSessionId, sourceSessionId) {
|
|
1671
|
+
const child = childSessionId.trim();
|
|
1672
|
+
const source = sourceSessionId.trim();
|
|
1673
|
+
if (child === "" || source === "" || child === source) return;
|
|
1674
|
+
pending.delete(child);
|
|
1675
|
+
pending.set(child, source);
|
|
1676
|
+
while (pending.size > MAX_PENDING) {
|
|
1677
|
+
const oldest = pending.keys().next();
|
|
1678
|
+
if (oldest.done === true) break;
|
|
1679
|
+
pending.delete(oldest.value);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function pendingForkSources(sessionId) {
|
|
1683
|
+
if (sessionId === void 0 || sessionId === "") return [];
|
|
1684
|
+
const source = pending.get(sessionId);
|
|
1685
|
+
return source === void 0 ? [] : [source];
|
|
1686
|
+
}
|
|
1687
|
+
function clearForkInheritancePending(childSessionId) {
|
|
1688
|
+
pending.delete(childSessionId);
|
|
1689
|
+
}
|
|
1690
|
+
function clearAllForkInheritancePending() {
|
|
1691
|
+
pending.clear();
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/host/integration/session-scope.ts
|
|
1654
1695
|
var STORE_KEY = /* @__PURE__ */ Symbol.for("dsh-over-ssh/session-scope");
|
|
1655
1696
|
function store() {
|
|
1656
1697
|
const holder = globalThis;
|
|
@@ -1670,6 +1711,15 @@ var MAX_FORK_DEPTH = 32;
|
|
|
1670
1711
|
var ancestorCache = /* @__PURE__ */ new Map();
|
|
1671
1712
|
var ANCESTOR_CACHE_MAX = 256;
|
|
1672
1713
|
function readSessionAncestors(ctx, sessionId) {
|
|
1714
|
+
const pending2 = pendingForkSources(sessionId);
|
|
1715
|
+
if (pending2.length > 0) {
|
|
1716
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
1717
|
+
for (const source of pending2) {
|
|
1718
|
+
inherited.add(source);
|
|
1719
|
+
for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
|
|
1720
|
+
}
|
|
1721
|
+
return [...inherited];
|
|
1722
|
+
}
|
|
1673
1723
|
const cached2 = ancestorCache.get(sessionId);
|
|
1674
1724
|
if (cached2 !== void 0) {
|
|
1675
1725
|
ancestorCache.delete(sessionId);
|
|
@@ -2193,6 +2243,7 @@ var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
|
2193
2243
|
var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
|
|
2194
2244
|
var MAX_EXEC_TIMEOUT_MS = 3e5;
|
|
2195
2245
|
var MAX_EXEC_ENV_ENTRIES = 64;
|
|
2246
|
+
var MAX_EXEC_BATCH_CONNECTIONS = 8;
|
|
2196
2247
|
var ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2197
2248
|
function stringArg(value) {
|
|
2198
2249
|
if (typeof value !== "string") return void 0;
|
|
@@ -3016,16 +3067,16 @@ function createTransferService(dependencies) {
|
|
|
3016
3067
|
};
|
|
3017
3068
|
const partialBytesOnRemote = async (absolute, context, signal) => {
|
|
3018
3069
|
try {
|
|
3019
|
-
const
|
|
3020
|
-
return
|
|
3070
|
+
const stat7 = await dependencies.statRemote(absolute);
|
|
3071
|
+
return stat7.exists && stat7.isFile ? stat7.size : void 0;
|
|
3021
3072
|
} catch (error) {
|
|
3022
3073
|
throw failureFrom(error, "remote-stat-failed", "\u8BFB\u53D6\u8FDC\u7AEF\u76EE\u6807\u4FE1\u606F\u5931\u8D25\uFF08\u7EED\u4F20\u524D\u9700\u8981\u786E\u8BA4\u5DF2\u6709\u957F\u5EA6\uFF09", { ...context }, signal);
|
|
3023
3074
|
}
|
|
3024
3075
|
};
|
|
3025
3076
|
const partialBytesOnLocal = async (path, context, signal) => {
|
|
3026
3077
|
try {
|
|
3027
|
-
const
|
|
3028
|
-
return
|
|
3078
|
+
const stat7 = await dependencies.statLocal(path);
|
|
3079
|
+
return stat7.exists && stat7.isFile ? stat7.size : void 0;
|
|
3029
3080
|
} catch (error) {
|
|
3030
3081
|
throw failureFrom(error, "local-stat-failed", "\u8BFB\u53D6\u672C\u5730\u76EE\u6807\u4FE1\u606F\u5931\u8D25\uFF08\u7EED\u4F20\u524D\u9700\u8981\u786E\u8BA4\u5DF2\u6709\u957F\u5EA6\uFF09", { ...context }, signal);
|
|
3031
3082
|
}
|
|
@@ -3126,7 +3177,13 @@ function createTransferService(dependencies) {
|
|
|
3126
3177
|
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
3127
3178
|
})
|
|
3128
3179
|
});
|
|
3129
|
-
const
|
|
3180
|
+
const degradations = [...decision.degradations];
|
|
3181
|
+
let digest = { sha256: void 0, degradation: void 0 };
|
|
3182
|
+
const needsLocalDigest = move.writerSha256 !== void 0 || decision.resumed;
|
|
3183
|
+
if (needsLocalDigest) {
|
|
3184
|
+
digest = await lookupDigest(dependencies, localPath);
|
|
3185
|
+
if (digest.degradation !== void 0) degradations.push(digest.degradation);
|
|
3186
|
+
}
|
|
3130
3187
|
if (move.writerSha256 !== void 0 && digest.sha256 !== void 0 && !digestsMatch(move.writerSha256, digest.sha256)) {
|
|
3131
3188
|
throw transferFailure(
|
|
3132
3189
|
"verify-failed",
|
|
@@ -3134,12 +3191,15 @@ function createTransferService(dependencies) {
|
|
|
3134
3191
|
{ ...context, bytesTransferred: move.bytesTransferred, chunks: move.chunks, localSha256: digest.sha256, remoteSha256: move.writerSha256 }
|
|
3135
3192
|
);
|
|
3136
3193
|
}
|
|
3137
|
-
const degradations = [...decision.degradations, ...digest.degradation === void 0 ? [] : [digest.degradation]];
|
|
3138
3194
|
let landed = move.writerSha256;
|
|
3139
3195
|
if (decision.resumed && landed === void 0) {
|
|
3140
3196
|
const remote = await lookupRemoteDigest(dependencies, target.absolute, request.signal);
|
|
3141
3197
|
if (remote.degradation !== void 0) degradations.push(remote.degradation);
|
|
3142
3198
|
if (remote.sha256 !== void 0) {
|
|
3199
|
+
if (digest.sha256 === void 0) {
|
|
3200
|
+
digest = await lookupDigest(dependencies, localPath);
|
|
3201
|
+
if (digest.degradation !== void 0) degradations.push(digest.degradation);
|
|
3202
|
+
}
|
|
3143
3203
|
if (digest.sha256 !== void 0 && !digestsMatch(remote.sha256, digest.sha256)) {
|
|
3144
3204
|
throw transferFailure(
|
|
3145
3205
|
"verify-failed",
|
|
@@ -4182,7 +4242,7 @@ function planDelete(records, request) {
|
|
|
4182
4242
|
refusals.push({ id, reason: "unreadable" });
|
|
4183
4243
|
continue;
|
|
4184
4244
|
}
|
|
4185
|
-
if (record2.classification === "subsession") {
|
|
4245
|
+
if (record2.classification === "subsession" && !isOrphanSubsession(record2, byId)) {
|
|
4186
4246
|
deferred.push(id);
|
|
4187
4247
|
continue;
|
|
4188
4248
|
}
|
|
@@ -4203,6 +4263,11 @@ function planDelete(records, request) {
|
|
|
4203
4263
|
for (const id of ordered) bytes += byId.get(id)?.bytes ?? 0;
|
|
4204
4264
|
return { delete: ordered, refusals, bytes };
|
|
4205
4265
|
}
|
|
4266
|
+
function isOrphanSubsession(record2, byId) {
|
|
4267
|
+
if (record2.parentSessionId === void 0) return true;
|
|
4268
|
+
const parent = byId.get(record2.parentSessionId);
|
|
4269
|
+
return parent === void 0 || parent.classification === "unreadable";
|
|
4270
|
+
}
|
|
4206
4271
|
function descendantsOf(records, rootId) {
|
|
4207
4272
|
const children = /* @__PURE__ */ new Map();
|
|
4208
4273
|
for (const record2 of records) {
|
|
@@ -4391,6 +4456,11 @@ function createSerialQueue() {
|
|
|
4391
4456
|
};
|
|
4392
4457
|
}
|
|
4393
4458
|
|
|
4459
|
+
// src/shared/path-fold.ts
|
|
4460
|
+
function foldPathKey(path) {
|
|
4461
|
+
return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4394
4464
|
// src/host/app/session-admin/session-admin-context.ts
|
|
4395
4465
|
function isMissing(error) {
|
|
4396
4466
|
const code = error?.code;
|
|
@@ -4406,18 +4476,18 @@ function createSessionAdminContext(deps) {
|
|
|
4406
4476
|
reason: "bin-inside-sessions-root"
|
|
4407
4477
|
});
|
|
4408
4478
|
}
|
|
4409
|
-
const
|
|
4479
|
+
const indexPath = () => joinPath(binRoot, SESSION_TRASH_INDEX_FILE);
|
|
4410
4480
|
const queue = createSerialQueue();
|
|
4411
4481
|
const serialized = (operation) => (
|
|
4412
4482
|
// Order matters: take the in-process slot **first** (the queue), then the cross-process lock. Holding a file
|
|
4413
4483
|
// lock while queued behind another in-process call would serialize on the wrong resource.
|
|
4414
|
-
queue.run(() => deps.withLock === void 0 ? operation() : deps.withLock(
|
|
4484
|
+
queue.run(() => deps.withLock === void 0 ? operation() : deps.withLock(indexPath(), operation))
|
|
4415
4485
|
);
|
|
4416
4486
|
const entryDir = (segment) => joinPath(binRoot, "entries", segment);
|
|
4417
4487
|
const readIndex = async () => {
|
|
4418
4488
|
let text8;
|
|
4419
4489
|
try {
|
|
4420
|
-
text8 = await deps.readText(
|
|
4490
|
+
text8 = await deps.readText(indexPath());
|
|
4421
4491
|
} catch (error) {
|
|
4422
4492
|
if (isMissing(error)) return [];
|
|
4423
4493
|
throw error;
|
|
@@ -4427,77 +4497,105 @@ function createSessionAdminContext(deps) {
|
|
|
4427
4497
|
const writeIndex = async (entries) => {
|
|
4428
4498
|
const text8 = serializeSessionTrashIndex(entries);
|
|
4429
4499
|
if (deps.writeTextAtomic !== void 0) {
|
|
4430
|
-
await deps.writeTextAtomic(
|
|
4500
|
+
await deps.writeTextAtomic(indexPath(), text8);
|
|
4431
4501
|
return;
|
|
4432
4502
|
}
|
|
4433
|
-
await deps.writeText(
|
|
4503
|
+
await deps.writeText(indexPath(), text8);
|
|
4504
|
+
};
|
|
4505
|
+
const INVENTORY_CONCURRENCY = 8;
|
|
4506
|
+
const mapWithLimit = async (items, limit, task) => {
|
|
4507
|
+
const results = new Array(items.length);
|
|
4508
|
+
let next = 0;
|
|
4509
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
4510
|
+
for (; ; ) {
|
|
4511
|
+
const index = next;
|
|
4512
|
+
next += 1;
|
|
4513
|
+
if (index >= items.length) return;
|
|
4514
|
+
results[index] = await task(items[index], index);
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
await Promise.all(workers);
|
|
4518
|
+
return results;
|
|
4519
|
+
};
|
|
4520
|
+
const throwIfAborted = (signal) => {
|
|
4521
|
+
if (signal?.aborted === true) {
|
|
4522
|
+
throw new SshDevError("CANCELLED", "\u64CD\u4F5C\u5DF2\u53D6\u6D88", { reason: "aborted" });
|
|
4523
|
+
}
|
|
4434
4524
|
};
|
|
4435
4525
|
const inventory = async (options = {}) => {
|
|
4436
|
-
|
|
4526
|
+
const signal = options.signal;
|
|
4527
|
+
throwIfAborted(signal);
|
|
4437
4528
|
const projects = await deps.listDir(sessionsRoot);
|
|
4438
|
-
const
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
const projectDir = joinPath(sessionsRoot, project.name);
|
|
4529
|
+
const projectNames = projects.filter((project) => project.isDirectory).map((project) => project.name);
|
|
4530
|
+
const perProject = await mapWithLimit(projectNames, INVENTORY_CONCURRENCY, async (project) => {
|
|
4531
|
+
const projectDir = joinPath(sessionsRoot, project);
|
|
4442
4532
|
const sessions = await deps.listDir(projectDir);
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
continue;
|
|
4471
|
-
}
|
|
4472
|
-
const title = header.title ?? deps.resolveSessionTitle?.(header);
|
|
4473
|
-
records.push({
|
|
4474
|
-
id: header.id,
|
|
4475
|
-
projectDir: project.name,
|
|
4533
|
+
return sessions.filter((session) => session.isDirectory).map((session) => ({ project, session }));
|
|
4534
|
+
});
|
|
4535
|
+
throwIfAborted(signal);
|
|
4536
|
+
const entries = perProject.flat();
|
|
4537
|
+
const workspaceTitles = /* @__PURE__ */ new Map();
|
|
4538
|
+
const workspaceTitleOf = (cwd) => {
|
|
4539
|
+
if (cwd === void 0) return void 0;
|
|
4540
|
+
if (!workspaceTitles.has(cwd)) workspaceTitles.set(cwd, deps.resolveWorkspaceTitle?.(cwd));
|
|
4541
|
+
return workspaceTitles.get(cwd);
|
|
4542
|
+
};
|
|
4543
|
+
return mapWithLimit(entries, INVENTORY_CONCURRENCY, async ({ project, session }) => {
|
|
4544
|
+
throwIfAborted(signal);
|
|
4545
|
+
const dir = joinPath(joinPath(sessionsRoot, project), session.name);
|
|
4546
|
+
const contents = await deps.listDir(dir);
|
|
4547
|
+
let bytes = 0;
|
|
4548
|
+
let updatedAtMs = Math.trunc(session.mtimeMs);
|
|
4549
|
+
for (const item of contents) {
|
|
4550
|
+
if (item.isDirectory) continue;
|
|
4551
|
+
bytes += item.bytes;
|
|
4552
|
+
const itemMs = Math.trunc(item.mtimeMs);
|
|
4553
|
+
if (itemMs > updatedAtMs) updatedAtMs = itemMs;
|
|
4554
|
+
}
|
|
4555
|
+
const header = await deps.readHeader(dir, contents);
|
|
4556
|
+
if (header === void 0) {
|
|
4557
|
+
return {
|
|
4558
|
+
id: session.name,
|
|
4559
|
+
projectDir: project,
|
|
4476
4560
|
segment: session.name,
|
|
4477
|
-
cwd:
|
|
4478
|
-
title,
|
|
4479
|
-
workspaceTitle:
|
|
4480
|
-
parentSessionId:
|
|
4561
|
+
cwd: void 0,
|
|
4562
|
+
title: void 0,
|
|
4563
|
+
workspaceTitle: void 0,
|
|
4564
|
+
parentSessionId: void 0,
|
|
4481
4565
|
bytes,
|
|
4482
4566
|
updatedAtMs,
|
|
4483
|
-
live:
|
|
4484
|
-
running:
|
|
4485
|
-
classification:
|
|
4486
|
-
}
|
|
4567
|
+
live: false,
|
|
4568
|
+
running: false,
|
|
4569
|
+
classification: "unreadable"
|
|
4570
|
+
};
|
|
4487
4571
|
}
|
|
4488
|
-
|
|
4489
|
-
|
|
4572
|
+
const title = header.title ?? deps.resolveSessionTitle?.(header);
|
|
4573
|
+
return {
|
|
4574
|
+
id: header.id,
|
|
4575
|
+
projectDir: project,
|
|
4576
|
+
segment: session.name,
|
|
4577
|
+
cwd: header.cwd,
|
|
4578
|
+
title,
|
|
4579
|
+
workspaceTitle: workspaceTitleOf(header.cwd),
|
|
4580
|
+
parentSessionId: header.parentSessionId,
|
|
4581
|
+
bytes,
|
|
4582
|
+
updatedAtMs,
|
|
4583
|
+
live: deps.isLive(header.id),
|
|
4584
|
+
running: deps.isRunning(header.id),
|
|
4585
|
+
classification: header.parentSessionId === void 0 ? "normal" : "subsession"
|
|
4586
|
+
};
|
|
4587
|
+
});
|
|
4490
4588
|
};
|
|
4491
|
-
return { deps, sessionsRoot, binRoot,
|
|
4589
|
+
return { deps, sessionsRoot, binRoot, indexPath, serialized, entryDir, readIndex, writeIndex, inventory, throwIfAborted };
|
|
4492
4590
|
}
|
|
4493
4591
|
function normalizePathKey(path) {
|
|
4494
|
-
return path
|
|
4592
|
+
return foldPathKey(path);
|
|
4495
4593
|
}
|
|
4496
4594
|
|
|
4497
4595
|
// src/host/app/session-admin/session-admin-delete.ts
|
|
4498
4596
|
init_errors();
|
|
4499
4597
|
function createSessionAdminDelete(ctx) {
|
|
4500
|
-
const { deps, sessionsRoot, serialized, entryDir, readIndex, writeIndex, inventory } = ctx;
|
|
4598
|
+
const { deps, sessionsRoot, serialized, entryDir, readIndex, writeIndex, inventory, throwIfAborted } = ctx;
|
|
4501
4599
|
const previewDelete = async (ids, options = {}) => {
|
|
4502
4600
|
const records = await inventory(options);
|
|
4503
4601
|
return planDelete(records, {
|
|
@@ -4505,10 +4603,11 @@ function createSessionAdminDelete(ctx) {
|
|
|
4505
4603
|
...options.keep === void 0 ? {} : { keep: options.keep }
|
|
4506
4604
|
});
|
|
4507
4605
|
};
|
|
4508
|
-
const deletePermanently = async (decision, byId) => {
|
|
4606
|
+
const deletePermanently = async (decision, byId, options = {}) => {
|
|
4509
4607
|
let bytes = 0;
|
|
4510
4608
|
const deleted = [];
|
|
4511
4609
|
for (const id of decision.delete) {
|
|
4610
|
+
throwIfAborted(options.signal);
|
|
4512
4611
|
const record2 = byId.get(id);
|
|
4513
4612
|
if (record2 === void 0) continue;
|
|
4514
4613
|
if (deps.isRunning(id)) {
|
|
@@ -4523,7 +4622,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4523
4622
|
}
|
|
4524
4623
|
return { mode: "permanent", entry: void 0, recoverable: false, verified: true, copied: false, refusals: decision.refusals, bytes };
|
|
4525
4624
|
};
|
|
4526
|
-
const moveIntoBin = async (decision, byId) => {
|
|
4625
|
+
const moveIntoBin = async (decision, byId, options = {}) => {
|
|
4527
4626
|
let bytes = 0;
|
|
4528
4627
|
let entry;
|
|
4529
4628
|
let verified = false;
|
|
@@ -4544,6 +4643,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4544
4643
|
};
|
|
4545
4644
|
try {
|
|
4546
4645
|
for (const id of decision.delete) {
|
|
4646
|
+
throwIfAborted(options.signal);
|
|
4547
4647
|
const record2 = byId.get(id);
|
|
4548
4648
|
if (record2 === void 0) continue;
|
|
4549
4649
|
if (deps.isRunning(id)) {
|
|
@@ -4631,7 +4731,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4631
4731
|
...options.keep === void 0 ? {} : { keep: options.keep }
|
|
4632
4732
|
});
|
|
4633
4733
|
const byId = new Map(records.map((record2) => [record2.id, record2]));
|
|
4634
|
-
return mode === "permanent" ? deletePermanently(decision, byId) : moveIntoBin(decision, byId);
|
|
4734
|
+
return mode === "permanent" ? deletePermanently(decision, byId, options) : moveIntoBin(decision, byId, options);
|
|
4635
4735
|
});
|
|
4636
4736
|
const purgeEntry = async (entryId) => {
|
|
4637
4737
|
const entries = await readIndex();
|
|
@@ -4755,7 +4855,6 @@ function createSessionAdminService(deps) {
|
|
|
4755
4855
|
const ordered = [...entries].sort((left, right) => left.deletedAtMs - right.deletedAtMs);
|
|
4756
4856
|
const doomed = [];
|
|
4757
4857
|
const nowMs = deps.now();
|
|
4758
|
-
let keptBytes = ordered.reduce((sum, item) => sum + item.bytes, 0);
|
|
4759
4858
|
if (policy.maxAgeMs > 0) {
|
|
4760
4859
|
for (const entry of ordered) {
|
|
4761
4860
|
if (nowMs - entry.deletedAtMs > policy.maxAgeMs) doomed.push(entry);
|
|
@@ -4764,6 +4863,7 @@ function createSessionAdminService(deps) {
|
|
|
4764
4863
|
const doomedIds = new Set(doomed.map((entry) => entry.id));
|
|
4765
4864
|
const survivors = ordered.filter((entry) => !doomedIds.has(entry.id));
|
|
4766
4865
|
if (policy.maxBytes > 0) {
|
|
4866
|
+
let keptBytes = survivors.reduce((sum, item) => sum + item.bytes, 0);
|
|
4767
4867
|
for (const entry of survivors) {
|
|
4768
4868
|
if (keptBytes <= policy.maxBytes) break;
|
|
4769
4869
|
doomed.push(entry);
|
|
@@ -4776,7 +4876,7 @@ function createSessionAdminService(deps) {
|
|
|
4776
4876
|
const excess = stillKept.length - policy.maxEntries;
|
|
4777
4877
|
for (const entry of stillKept.slice(0, excess)) {
|
|
4778
4878
|
doomed.push(entry);
|
|
4779
|
-
|
|
4879
|
+
doomedIds.add(entry.id);
|
|
4780
4880
|
}
|
|
4781
4881
|
}
|
|
4782
4882
|
const outcome = await purgeEntries(doomed, entries);
|
|
@@ -4792,11 +4892,54 @@ function createSessionAdminService(deps) {
|
|
|
4792
4892
|
}
|
|
4793
4893
|
|
|
4794
4894
|
// src/host/integration/session/session-header.ts
|
|
4795
|
-
import { open, readdir } from "node:fs/promises";
|
|
4895
|
+
import { open, readdir, stat } from "node:fs/promises";
|
|
4796
4896
|
import { join } from "node:path";
|
|
4797
4897
|
import { createZstdDecompress } from "node:zlib";
|
|
4798
4898
|
var SESSION_HEADER_READ_BYTES = 64 * 1024;
|
|
4799
4899
|
var SESSION_HEADER_RETRY_BYTES = 1024 * 1024;
|
|
4900
|
+
function createCachedSessionHeaderReader(io, options = {}) {
|
|
4901
|
+
const inner = createSessionHeaderReader(io);
|
|
4902
|
+
const stat7 = io.stat;
|
|
4903
|
+
if (stat7 === void 0) return inner;
|
|
4904
|
+
const maxEntries = Math.max(1, Math.floor(options.maxEntries ?? SESSION_HEADER_CACHE_MAX));
|
|
4905
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4906
|
+
return async (dir, listing) => {
|
|
4907
|
+
let signature;
|
|
4908
|
+
if (listing !== void 0) {
|
|
4909
|
+
const log = listing.find((entry) => entry.name.startsWith("session.v") && entry.name.endsWith(".zstd"));
|
|
4910
|
+
if (log !== void 0) signature = `${log.size}:${Math.trunc(log.mtimeMs)}`;
|
|
4911
|
+
} else {
|
|
4912
|
+
try {
|
|
4913
|
+
const names = await io.listDir(dir);
|
|
4914
|
+
const log = names.find((name2) => name2.startsWith("session.v") && name2.endsWith(".zstd"));
|
|
4915
|
+
if (log !== void 0) {
|
|
4916
|
+
const info = await stat7(join(dir, log));
|
|
4917
|
+
signature = `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
4918
|
+
}
|
|
4919
|
+
} catch {
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
if (signature !== void 0) {
|
|
4923
|
+
const hit = cache.get(dir);
|
|
4924
|
+
if (hit !== void 0 && hit.signature === signature) {
|
|
4925
|
+
cache.delete(dir);
|
|
4926
|
+
cache.set(dir, hit);
|
|
4927
|
+
return hit.header;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
const header = await inner(dir);
|
|
4931
|
+
if (signature !== void 0) {
|
|
4932
|
+
cache.set(dir, { signature, header });
|
|
4933
|
+
while (cache.size > maxEntries) {
|
|
4934
|
+
const oldest = cache.keys().next();
|
|
4935
|
+
if (oldest.done === true) break;
|
|
4936
|
+
cache.delete(oldest.value);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
return header;
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
var SESSION_HEADER_CACHE_MAX = 2048;
|
|
4800
4943
|
function parseSessionHeaderLine(text8) {
|
|
4801
4944
|
const firstLine = text8.split("\n")[0];
|
|
4802
4945
|
if (firstLine === void 0 || firstLine === "") return void 0;
|
|
@@ -4882,11 +5025,16 @@ function createDiskSessionHeaderIo() {
|
|
|
4882
5025
|
return {
|
|
4883
5026
|
listDir: (dir) => readdir(dir),
|
|
4884
5027
|
readHead: readHeadFromDisk,
|
|
4885
|
-
decompress: decompressPrefix
|
|
5028
|
+
decompress: decompressPrefix,
|
|
5029
|
+
// The cache's only input: `(size, mtime)` is what a session log's append changes.
|
|
5030
|
+
stat: async (path) => {
|
|
5031
|
+
const info = await stat(path);
|
|
5032
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
5033
|
+
}
|
|
4886
5034
|
};
|
|
4887
5035
|
}
|
|
4888
5036
|
function createDiskSessionHeaderReader() {
|
|
4889
|
-
return
|
|
5037
|
+
return createCachedSessionHeaderReader(createDiskSessionHeaderIo());
|
|
4890
5038
|
}
|
|
4891
5039
|
|
|
4892
5040
|
// src/host/app/routed-fs.ts
|
|
@@ -5772,17 +5920,25 @@ var RootRegistry = class {
|
|
|
5772
5920
|
* sessions at all: an empty list means "the session service was not readable", not "no session is live", and
|
|
5773
5921
|
* dropping every grant on a transient read failure would break live turns.
|
|
5774
5922
|
*
|
|
5775
|
-
* @param cwds - working directories of the live sessions.
|
|
5923
|
+
* @param cwds - working directories of the live sessions, optionally with the session id that owns each one.
|
|
5924
|
+
* With an id the root carries `sessions: [id]` and is writable **only by that session**; without one it is a
|
|
5925
|
+
* workspace-scoped root (the legacy shape tests and agentless compositions use).
|
|
5776
5926
|
* @returns the session roots this call added (empty when every cwd was already covered).
|
|
5777
5927
|
*/
|
|
5778
5928
|
syncSessionWorkspaces(cwds) {
|
|
5779
5929
|
if (cwds.length === 0) return [];
|
|
5780
5930
|
const live = /* @__PURE__ */ new Set();
|
|
5781
5931
|
const added = [];
|
|
5782
|
-
for (const
|
|
5932
|
+
for (const item of cwds) {
|
|
5933
|
+
const cwd = typeof item === "string" ? item : item.cwd;
|
|
5934
|
+
const sessionId = typeof item === "string" ? void 0 : item.sessionId;
|
|
5783
5935
|
if (typeof cwd !== "string" || cwd === "") continue;
|
|
5784
5936
|
if (!isAbsoluteLocalPath(normalizeLocalPath2(cwd, { caseInsensitive: false }))) continue;
|
|
5785
|
-
const root = makeLocalRoot(cwd, {
|
|
5937
|
+
const root = makeLocalRoot(cwd, {
|
|
5938
|
+
workspaces: [cwd],
|
|
5939
|
+
...sessionId === void 0 || sessionId === "" ? {} : { sessions: [sessionId] },
|
|
5940
|
+
writable: true
|
|
5941
|
+
});
|
|
5786
5942
|
const key = rootKey(root);
|
|
5787
5943
|
live.add(key);
|
|
5788
5944
|
if (this.#localRootCovers(cwd)) continue;
|
|
@@ -5959,7 +6115,7 @@ function carryMissingState(legacy, current, files, dirs) {
|
|
|
5959
6115
|
renameSync(temp, to);
|
|
5960
6116
|
} catch (error) {
|
|
5961
6117
|
rmSync(temp, { force: true });
|
|
5962
|
-
failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6118
|
+
if (!existsSync(to)) failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
5963
6119
|
}
|
|
5964
6120
|
}
|
|
5965
6121
|
for (const name2 of dirs) {
|
|
@@ -5972,7 +6128,7 @@ function carryMissingState(legacy, current, files, dirs) {
|
|
|
5972
6128
|
renameSync(temp, to);
|
|
5973
6129
|
} catch (error) {
|
|
5974
6130
|
rmSync(temp, { recursive: true, force: true });
|
|
5975
|
-
failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
6131
|
+
if (!existsSync(to)) failures.push(`${from} \u2192 ${to}\uFF1A${describe(error)}`);
|
|
5976
6132
|
}
|
|
5977
6133
|
}
|
|
5978
6134
|
return failures;
|
|
@@ -6398,9 +6554,9 @@ var AgentFrameParser = class {
|
|
|
6398
6554
|
*/
|
|
6399
6555
|
#append(chunk) {
|
|
6400
6556
|
if (chunk.byteLength === 0) return;
|
|
6401
|
-
const
|
|
6402
|
-
if (this.#store.byteLength - this.#start <
|
|
6403
|
-
const needed =
|
|
6557
|
+
const pending2 = this.#end - this.#start;
|
|
6558
|
+
if (this.#store.byteLength - this.#start < pending2 + chunk.byteLength) {
|
|
6559
|
+
const needed = pending2 + chunk.byteLength;
|
|
6404
6560
|
if (this.#store.byteLength >= needed) {
|
|
6405
6561
|
this.#store.copyWithin(0, this.#start, this.#end);
|
|
6406
6562
|
} else {
|
|
@@ -6409,7 +6565,7 @@ var AgentFrameParser = class {
|
|
|
6409
6565
|
this.#store = grown;
|
|
6410
6566
|
}
|
|
6411
6567
|
this.#start = 0;
|
|
6412
|
-
this.#end =
|
|
6568
|
+
this.#end = pending2;
|
|
6413
6569
|
}
|
|
6414
6570
|
this.#store.set(chunk, this.#end);
|
|
6415
6571
|
this.#end += chunk.byteLength;
|
|
@@ -7194,9 +7350,9 @@ var AgentProtocolClient = class {
|
|
|
7194
7350
|
failConnection(error) {
|
|
7195
7351
|
if (this.#lost !== void 0) return;
|
|
7196
7352
|
this.#lost = error;
|
|
7197
|
-
const
|
|
7353
|
+
const pending2 = [...this.#pending.values()];
|
|
7198
7354
|
this.#pending.clear();
|
|
7199
|
-
for (const entry of
|
|
7355
|
+
for (const entry of pending2) entry.settle({ error });
|
|
7200
7356
|
for (const channel of this.#blobQueue) channel.fail(error);
|
|
7201
7357
|
this.#blobQueue.length = 0;
|
|
7202
7358
|
this.#blobById.clear();
|
|
@@ -8081,14 +8237,14 @@ var AgentTransport = class extends TransportBase {
|
|
|
8081
8237
|
}
|
|
8082
8238
|
}
|
|
8083
8239
|
if (expectedVersion === void 0) {
|
|
8084
|
-
let
|
|
8240
|
+
let stat7;
|
|
8085
8241
|
try {
|
|
8086
|
-
|
|
8242
|
+
stat7 = asRecord(await client.request("fs.stat", { path: request.path }, this.#requestOptions(void 0, signal)));
|
|
8087
8243
|
} catch (error) {
|
|
8088
8244
|
throw this.#translate(error);
|
|
8089
8245
|
}
|
|
8090
|
-
const exists =
|
|
8091
|
-
const observed = typeof
|
|
8246
|
+
const exists = stat7?.["exists"] === true;
|
|
8247
|
+
const observed = typeof stat7?.["version"] === "string" ? stat7["version"] : void 0;
|
|
8092
8248
|
if (!exists || observed === void 0 || observed !== expected) {
|
|
8093
8249
|
throw new SshDevError("TRANSFER_CONFLICT", "\u8FDC\u7AEF\u6587\u4EF6\u7248\u672C\u4E0E\u8C03\u7528\u65B9\u7ED9\u51FA\u7684\u5B88\u536B\u4E0D\u4E00\u81F4\uFF0C\u5199\u5165\u88AB\u62D2\u7EDD", {
|
|
8094
8250
|
connectionId: this.id,
|
|
@@ -9346,7 +9502,7 @@ function localVersion(info) {
|
|
|
9346
9502
|
init_temp_name();
|
|
9347
9503
|
init_errors();
|
|
9348
9504
|
import { constants } from "node:fs";
|
|
9349
|
-
import { access, chmod, lstat, mkdir, open as open2, readdir as readdir2, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
9505
|
+
import { access, chmod, lstat, mkdir, open as open2, readdir as readdir2, rename, rm, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
9350
9506
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
9351
9507
|
var LIST_STAT_CONCURRENCY = 32;
|
|
9352
9508
|
var LocalBackend = class {
|
|
@@ -9359,12 +9515,12 @@ var LocalBackend = class {
|
|
|
9359
9515
|
this.id = options.id;
|
|
9360
9516
|
this.sandboxMode = options.sandboxMode ?? "workspace-write";
|
|
9361
9517
|
this.#maxReadBytes = options.maxReadBytes ?? 16 * 1024 * 1024;
|
|
9362
|
-
this.#statEntry = options.statEntry ?? ((path) =>
|
|
9518
|
+
this.#statEntry = options.statEntry ?? ((path) => stat2(path));
|
|
9363
9519
|
}
|
|
9364
9520
|
async stat(absolute, signal) {
|
|
9365
9521
|
signal?.throwIfAborted();
|
|
9366
9522
|
try {
|
|
9367
|
-
const info = await
|
|
9523
|
+
const info = await stat2(absolute);
|
|
9368
9524
|
return {
|
|
9369
9525
|
exists: true,
|
|
9370
9526
|
isDirectory: info.isDirectory(),
|
|
@@ -10021,7 +10177,9 @@ function buildListScript(absolute) {
|
|
|
10021
10177
|
`if [ ! -d "$p" ]; then printf 'DSH_ERROR=not-a-dir\\n'; exit 4; fi`,
|
|
10022
10178
|
// GNU find (Linux). `-printf ''` is the cheap capability probe: it fails on BSD/macOS find.
|
|
10023
10179
|
`if find "$p" -maxdepth 1 -printf '' >/dev/null 2>&1; then`,
|
|
10024
|
-
|
|
10180
|
+
// **NUL 分隔**:文件名可以含换行(POSIX 合法,`git checkout` 就能造出来),按行切会把一个条目拆成
|
|
10181
|
+
// 两行、解析出一个不存在的名字并丢掉真条目(审计 2026-11-06 第 3 轮 P1)。
|
|
10182
|
+
` find "$p" -mindepth 1 -maxdepth 1 -printf '%y\\t%Y\\t%s\\t%T@\\t%f\\0'`,
|
|
10025
10183
|
`else`,
|
|
10026
10184
|
` for entry in "$p"/* "$p"/.[!.]*; do`,
|
|
10027
10185
|
` [ -e "$entry" ] || continue`,
|
|
@@ -10029,28 +10187,28 @@ function buildListScript(absolute) {
|
|
|
10029
10187
|
` if [ -d "$entry" ]; then kind=d; elif [ -L "$entry" ]; then kind=l; else kind=f; fi`,
|
|
10030
10188
|
` size=$(wc -c < "$entry" 2>/dev/null | tr -d ' ' || echo 0)`,
|
|
10031
10189
|
` mtime=$(stat -c '%Y' "$entry" 2>/dev/null || stat -f '%m' "$entry" 2>/dev/null || echo 0)`,
|
|
10032
|
-
` printf '%s\\t%s\\t%s\\t%s\\t%s\\
|
|
10190
|
+
` printf '%s\\t%s\\t%s\\t%s\\t%s\\0' "$kind" "$kind" "$size" "$mtime" "$name"`,
|
|
10033
10191
|
` done`,
|
|
10034
10192
|
`fi`
|
|
10035
10193
|
].join("\n");
|
|
10036
10194
|
}
|
|
10037
10195
|
function parseListing(stdout) {
|
|
10038
10196
|
const entries = [];
|
|
10039
|
-
for (const
|
|
10040
|
-
if (
|
|
10041
|
-
const parts =
|
|
10197
|
+
for (const record2 of stdout.split("\0")) {
|
|
10198
|
+
if (record2 === "" || record2.startsWith("DSH_")) continue;
|
|
10199
|
+
const parts = record2.split(" ");
|
|
10042
10200
|
if (parts.length < 5) continue;
|
|
10043
10201
|
const [typeChar, targetChar, sizeText, mtimeText] = parts;
|
|
10044
10202
|
const kind = typeChar === "d" ? "d" : typeChar === "l" ? targetChar === "d" ? "d" : "l" : "f";
|
|
10045
10203
|
const size = Number.parseInt(sizeText, 10);
|
|
10046
|
-
const seconds = Number.
|
|
10204
|
+
const seconds = Number.parseFloat(mtimeText);
|
|
10047
10205
|
entries.push({
|
|
10048
10206
|
name: parts.slice(4).join(" "),
|
|
10049
10207
|
isDirectory: kind === "d",
|
|
10050
10208
|
isFile: kind === "f",
|
|
10051
10209
|
isSymlink: kind === "l",
|
|
10052
10210
|
size: Number.isFinite(size) ? size : 0,
|
|
10053
|
-
mtimeMs:
|
|
10211
|
+
mtimeMs: Number.isFinite(seconds) ? Math.trunc(seconds * 1e3) : 0
|
|
10054
10212
|
});
|
|
10055
10213
|
}
|
|
10056
10214
|
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
@@ -10438,6 +10596,13 @@ var RemoteBackend = class {
|
|
|
10438
10596
|
if (result.stdout.includes("DSH_ERROR=not-a-dir")) {
|
|
10439
10597
|
throw new SshDevError("REMOTE_NOT_FOUND", `\u8FDC\u7AEF\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${absolute}`, { path: absolute });
|
|
10440
10598
|
}
|
|
10599
|
+
if (result.truncated === true) {
|
|
10600
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u76EE\u5F55\u6761\u76EE\u8FC7\u591A\uFF0C\u5217\u4E3E\u7ED3\u679C\u88AB\u622A\u65AD\uFF1A${absolute}`, {
|
|
10601
|
+
path: absolute,
|
|
10602
|
+
reason: "listing-truncated",
|
|
10603
|
+
maxBytes: 4 * 1024 * 1024
|
|
10604
|
+
});
|
|
10605
|
+
}
|
|
10441
10606
|
return parseListing(result.stdout);
|
|
10442
10607
|
}
|
|
10443
10608
|
/**
|
|
@@ -11079,7 +11244,21 @@ var RemoteBackend = class {
|
|
|
11079
11244
|
}
|
|
11080
11245
|
const command = tool === "sha256sum" ? "sha256sum" : "shasum -a 256";
|
|
11081
11246
|
const result = await (await this.#transport()).exec({ argv: ["sh", "-c", `${command} -- "$1" | cut -d' ' -f1`, "dsh-digest", absolute] }, signal);
|
|
11082
|
-
|
|
11247
|
+
if (result.exitCode !== 0) {
|
|
11248
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u65E0\u6CD5\u8BA1\u7B97\u8FDC\u7AEF\u6587\u4EF6\u7684 sha256\uFF08\u6458\u8981\u547D\u4EE4\u9000\u51FA\u7801 ${result.exitCode}\uFF09\uFF1A${absolute}`, {
|
|
11249
|
+
path: absolute,
|
|
11250
|
+
exitCode: result.exitCode,
|
|
11251
|
+
stderr: result.stderr.slice(0, 200)
|
|
11252
|
+
});
|
|
11253
|
+
}
|
|
11254
|
+
const digest = result.stdout.trim();
|
|
11255
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) {
|
|
11256
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", `\u8FDC\u7AEF\u6458\u8981\u5DE5\u5177\u8FD4\u56DE\u4E86\u975E\u6CD5\u6458\u8981\uFF08\u4E0D\u662F 64 \u4F4D\u5C0F\u5199\u5341\u516D\u8FDB\u5236\uFF09\uFF1A${absolute}`, {
|
|
11257
|
+
path: absolute,
|
|
11258
|
+
digest: digest.slice(0, 120)
|
|
11259
|
+
});
|
|
11260
|
+
}
|
|
11261
|
+
return digest;
|
|
11083
11262
|
}
|
|
11084
11263
|
};
|
|
11085
11264
|
function sha256Hex(bytes) {
|
|
@@ -11910,12 +12089,12 @@ init_ssh2_transport();
|
|
|
11910
12089
|
|
|
11911
12090
|
// src/host/integration/agent/agent-install-io.ts
|
|
11912
12091
|
init_shell_quote();
|
|
11913
|
-
import { readFile as readFile2, stat as
|
|
12092
|
+
import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
|
|
11914
12093
|
var DEFAULT_MAX_UPLOAD_BYTES = 32 * 1024 * 1024;
|
|
11915
12094
|
function createAgentInstallIo(options) {
|
|
11916
12095
|
const maxUploadBytes = options.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
11917
12096
|
const readLocal = options.readLocalFile ?? ((path) => readFile2(path).then((buffer) => new Uint8Array(buffer)));
|
|
11918
|
-
const sizeOf = options.localSize ?? ((path) =>
|
|
12097
|
+
const sizeOf = options.localSize ?? ((path) => stat3(path).then((info) => info.size));
|
|
11919
12098
|
const exec = async (command, signal) => {
|
|
11920
12099
|
const result = await options.transport.exec(
|
|
11921
12100
|
{ argv: ["/bin/sh", "-c", command], maxBytes: options.probeMaxBytes ?? 256 * 1024 },
|
|
@@ -11998,14 +12177,14 @@ function reviseToTier1(profile, decision, reason, hint) {
|
|
|
11998
12177
|
// src/host/integration/agent/askpass-registry.ts
|
|
11999
12178
|
function createAskpassRegistry(options) {
|
|
12000
12179
|
const handles = /* @__PURE__ */ new Map();
|
|
12001
|
-
const
|
|
12180
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
12002
12181
|
const retired = /* @__PURE__ */ new Set();
|
|
12003
12182
|
const prepare = async (profile) => {
|
|
12004
12183
|
if (!needsAskpass(profile.auth.kind)) return void 0;
|
|
12005
12184
|
if (retired.has(profile.id)) return void 0;
|
|
12006
12185
|
const existing = handles.get(profile.id);
|
|
12007
12186
|
if (existing !== void 0) return existing.env;
|
|
12008
|
-
const inflight =
|
|
12187
|
+
const inflight = pending2.get(profile.id);
|
|
12009
12188
|
if (inflight !== void 0) return inflight;
|
|
12010
12189
|
const attempt = (async () => {
|
|
12011
12190
|
try {
|
|
@@ -12022,16 +12201,16 @@ function createAskpassRegistry(options) {
|
|
|
12022
12201
|
return void 0;
|
|
12023
12202
|
}
|
|
12024
12203
|
})().finally(() => {
|
|
12025
|
-
|
|
12204
|
+
pending2.delete(profile.id);
|
|
12026
12205
|
});
|
|
12027
|
-
|
|
12206
|
+
pending2.set(profile.id, attempt);
|
|
12028
12207
|
return attempt;
|
|
12029
12208
|
};
|
|
12030
12209
|
const forget = async (connectionId) => {
|
|
12031
12210
|
retired.add(connectionId);
|
|
12032
12211
|
const handle = handles.get(connectionId);
|
|
12033
12212
|
handles.delete(connectionId);
|
|
12034
|
-
|
|
12213
|
+
pending2.delete(connectionId);
|
|
12035
12214
|
if (handle !== void 0) await handle.dispose().catch(() => void 0);
|
|
12036
12215
|
};
|
|
12037
12216
|
return {
|
|
@@ -12049,7 +12228,7 @@ function createAskpassRegistry(options) {
|
|
|
12049
12228
|
retired.delete(connectionId);
|
|
12050
12229
|
},
|
|
12051
12230
|
disposeAll: async () => {
|
|
12052
|
-
const ids = /* @__PURE__ */ new Set([...handles.keys(), ...
|
|
12231
|
+
const ids = /* @__PURE__ */ new Set([...handles.keys(), ...pending2.keys()]);
|
|
12053
12232
|
for (const id of ids) retired.add(id);
|
|
12054
12233
|
await Promise.all([...ids].map((id) => forget(id)));
|
|
12055
12234
|
}
|
|
@@ -12166,6 +12345,27 @@ var RemoteIndex = class {
|
|
|
12166
12345
|
* root is correct, just slower.
|
|
12167
12346
|
*/
|
|
12168
12347
|
#scannedOnce = /* @__PURE__ */ new Set();
|
|
12348
|
+
/**
|
|
12349
|
+
* In-flight enumerations, so a burst of callers on a cold root does **one** scan.
|
|
12350
|
+
*
|
|
12351
|
+
* 审计 2026-11-05 ts.md P1:`enumerate` 没有 single-flight——同一次键入可能同时发出 `index.scan` 与
|
|
12352
|
+
* `index.find`(file-reference 一次 list 最多两个枚举,可指向同一 root),于是两次远端扫描都跑、
|
|
12353
|
+
* 结果只留一份。键里带 signal 身份:不同请求的取消不能互相传染(A 的 abort 不该杀掉 B 的扫描)。
|
|
12354
|
+
*/
|
|
12355
|
+
#inFlight = /* @__PURE__ */ new Map();
|
|
12356
|
+
/** Stable identity per `AbortSignal`, only used to keep in-flight keys apart. */
|
|
12357
|
+
#signalIds = /* @__PURE__ */ new WeakMap();
|
|
12358
|
+
#nextSignalId = 0;
|
|
12359
|
+
#inFlightKey(root, signal) {
|
|
12360
|
+
if (signal === void 0) return `${root}\0`;
|
|
12361
|
+
let id = this.#signalIds.get(signal);
|
|
12362
|
+
if (id === void 0) {
|
|
12363
|
+
this.#nextSignalId += 1;
|
|
12364
|
+
id = this.#nextSignalId;
|
|
12365
|
+
this.#signalIds.set(signal, id);
|
|
12366
|
+
}
|
|
12367
|
+
return `${root}\0${id}`;
|
|
12368
|
+
}
|
|
12169
12369
|
constructor(options) {
|
|
12170
12370
|
this.#tiers = options.tiers;
|
|
12171
12371
|
this.#ttlMs = options.ttlMs ?? DEFAULTS.ttlMs;
|
|
@@ -12195,9 +12395,20 @@ var RemoteIndex = class {
|
|
|
12195
12395
|
};
|
|
12196
12396
|
}
|
|
12197
12397
|
}
|
|
12198
|
-
const
|
|
12199
|
-
|
|
12200
|
-
return
|
|
12398
|
+
const key = this.#inFlightKey(root, options.signal);
|
|
12399
|
+
const running = this.#inFlight.get(key);
|
|
12400
|
+
if (running !== void 0) return running;
|
|
12401
|
+
const attempt = (async () => {
|
|
12402
|
+
const result = await this.#enumerateFresh(root, maxEntries, options.budgetMs ?? this.#budgetMs, options.signal);
|
|
12403
|
+
if (result.cacheable) this.#remember(root, result);
|
|
12404
|
+
return result;
|
|
12405
|
+
})();
|
|
12406
|
+
this.#inFlight.set(key, attempt);
|
|
12407
|
+
try {
|
|
12408
|
+
return await attempt;
|
|
12409
|
+
} finally {
|
|
12410
|
+
if (this.#inFlight.get(key) === attempt) this.#inFlight.delete(key);
|
|
12411
|
+
}
|
|
12201
12412
|
}
|
|
12202
12413
|
/**
|
|
12203
12414
|
* Completion for a partially typed `@` query.
|
|
@@ -12712,6 +12923,129 @@ import { join as join9 } from "node:path";
|
|
|
12712
12923
|
import { homedir as homedir2 } from "node:os";
|
|
12713
12924
|
import { createRequire } from "node:module";
|
|
12714
12925
|
import "@deepseek-ai/cordis";
|
|
12926
|
+
|
|
12927
|
+
// src/host/domain/roots/ssh-config.ts
|
|
12928
|
+
function stripComment(line) {
|
|
12929
|
+
let inQuotes = false;
|
|
12930
|
+
for (let index = 0; index < line.length; index++) {
|
|
12931
|
+
const char = line[index];
|
|
12932
|
+
if (char === '"') inQuotes = !inQuotes;
|
|
12933
|
+
else if (char === "#" && !inQuotes) return line.slice(0, index);
|
|
12934
|
+
}
|
|
12935
|
+
return line;
|
|
12936
|
+
}
|
|
12937
|
+
function unquote(value) {
|
|
12938
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
12939
|
+
return value;
|
|
12940
|
+
}
|
|
12941
|
+
function splitKeyValue(line) {
|
|
12942
|
+
const match = /^([A-Za-z][A-Za-z0-9]*)\s*(?:=|\s)\s*(.*)$/.exec(line);
|
|
12943
|
+
if (match === null) return void 0;
|
|
12944
|
+
const key = match[1];
|
|
12945
|
+
const value = match[2];
|
|
12946
|
+
if (key === void 0 || value === void 0) return void 0;
|
|
12947
|
+
return [key.toLowerCase(), unquote(value.trim())];
|
|
12948
|
+
}
|
|
12949
|
+
function parseSshConfig(text8) {
|
|
12950
|
+
const entries = [];
|
|
12951
|
+
let current;
|
|
12952
|
+
const lines = text8.split(/\r?\n/);
|
|
12953
|
+
for (let index = 0; index < lines.length; index++) {
|
|
12954
|
+
const raw = lines[index] ?? "";
|
|
12955
|
+
const line = stripComment(raw).trim();
|
|
12956
|
+
if (line === "") continue;
|
|
12957
|
+
const pair = splitKeyValue(line);
|
|
12958
|
+
if (pair === void 0) continue;
|
|
12959
|
+
const [key, value] = pair;
|
|
12960
|
+
if (key === "host") {
|
|
12961
|
+
const patterns = value.split(/\s+/).filter((part) => part !== "");
|
|
12962
|
+
if (patterns.length === 0) continue;
|
|
12963
|
+
current = { patterns, identityFiles: [], includes: [], line: index + 1 };
|
|
12964
|
+
entries.push(current);
|
|
12965
|
+
continue;
|
|
12966
|
+
}
|
|
12967
|
+
if (current === void 0) continue;
|
|
12968
|
+
switch (key) {
|
|
12969
|
+
case "hostname":
|
|
12970
|
+
current.hostName ??= value;
|
|
12971
|
+
break;
|
|
12972
|
+
case "user":
|
|
12973
|
+
current.user ??= value;
|
|
12974
|
+
break;
|
|
12975
|
+
case "port": {
|
|
12976
|
+
if (current.port !== void 0) break;
|
|
12977
|
+
const port = Number.parseInt(value, 10);
|
|
12978
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) current.port = port;
|
|
12979
|
+
break;
|
|
12980
|
+
}
|
|
12981
|
+
case "identityfile":
|
|
12982
|
+
current.identityFiles.push(value);
|
|
12983
|
+
break;
|
|
12984
|
+
case "proxyjump":
|
|
12985
|
+
current.proxyJump ??= value.toLowerCase() === "none" ? "" : value;
|
|
12986
|
+
break;
|
|
12987
|
+
case "include":
|
|
12988
|
+
current.includes.push(value);
|
|
12989
|
+
break;
|
|
12990
|
+
default:
|
|
12991
|
+
break;
|
|
12992
|
+
}
|
|
12993
|
+
}
|
|
12994
|
+
return entries.map((entry) => ({
|
|
12995
|
+
patterns: entry.patterns,
|
|
12996
|
+
...entry.hostName === void 0 ? {} : { hostName: entry.hostName },
|
|
12997
|
+
...entry.user === void 0 ? {} : { user: entry.user },
|
|
12998
|
+
...entry.port === void 0 ? {} : { port: entry.port },
|
|
12999
|
+
identityFiles: entry.identityFiles,
|
|
13000
|
+
...entry.proxyJump === void 0 || entry.proxyJump === "" ? {} : { proxyJump: entry.proxyJump },
|
|
13001
|
+
includes: entry.includes,
|
|
13002
|
+
line: entry.line
|
|
13003
|
+
}));
|
|
13004
|
+
}
|
|
13005
|
+
function isConcretePattern(pattern) {
|
|
13006
|
+
return !/[*?!]/.test(pattern);
|
|
13007
|
+
}
|
|
13008
|
+
function expandHome(path, home) {
|
|
13009
|
+
if (path === "~") return home;
|
|
13010
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return `${home.replace(/[\\/]+$/, "")}${path.slice(1)}`;
|
|
13011
|
+
return path;
|
|
13012
|
+
}
|
|
13013
|
+
function importableEntries(entries, options = {}) {
|
|
13014
|
+
const home = options.home ?? "";
|
|
13015
|
+
const excluded = new Set(options.excluded ?? []);
|
|
13016
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13017
|
+
const drafts = [];
|
|
13018
|
+
const skipped = [];
|
|
13019
|
+
for (const entry of entries) {
|
|
13020
|
+
for (const pattern of entry.patterns) {
|
|
13021
|
+
if (!isConcretePattern(pattern)) {
|
|
13022
|
+
skipped.push({ pattern, reason: "wildcard", line: entry.line });
|
|
13023
|
+
continue;
|
|
13024
|
+
}
|
|
13025
|
+
if (seen.has(pattern)) {
|
|
13026
|
+
skipped.push({ pattern, reason: "duplicate", line: entry.line });
|
|
13027
|
+
continue;
|
|
13028
|
+
}
|
|
13029
|
+
if (excluded.has(pattern)) {
|
|
13030
|
+
skipped.push({ pattern, reason: "already-imported", line: entry.line });
|
|
13031
|
+
continue;
|
|
13032
|
+
}
|
|
13033
|
+
seen.add(pattern);
|
|
13034
|
+
const identityFile = entry.identityFiles[0];
|
|
13035
|
+
drafts.push({
|
|
13036
|
+
alias: pattern,
|
|
13037
|
+
hostName: entry.hostName ?? pattern,
|
|
13038
|
+
...entry.user === void 0 ? {} : { user: entry.user },
|
|
13039
|
+
...entry.port === void 0 ? {} : { port: entry.port },
|
|
13040
|
+
...identityFile === void 0 ? {} : { identityFile: home === "" ? identityFile : expandHome(identityFile, home) },
|
|
13041
|
+
...entry.proxyJump === void 0 ? {} : { proxyJump: entry.proxyJump }
|
|
13042
|
+
});
|
|
13043
|
+
}
|
|
13044
|
+
}
|
|
13045
|
+
return { drafts, skipped };
|
|
13046
|
+
}
|
|
13047
|
+
|
|
13048
|
+
// src/wiring/helpers.ts
|
|
12715
13049
|
init_errors();
|
|
12716
13050
|
init_ssh2_transport();
|
|
12717
13051
|
|
|
@@ -12800,7 +13134,7 @@ async function resolveAuth(ctx, profile) {
|
|
|
12800
13134
|
return agent === void 0 ? {} : { agent };
|
|
12801
13135
|
}
|
|
12802
13136
|
if (spec.kind === "key") {
|
|
12803
|
-
const path =
|
|
13137
|
+
const path = expandHome2(spec.keyPath);
|
|
12804
13138
|
const privateKey = await readFile4(path).catch((error) => {
|
|
12805
13139
|
const message = error instanceof Error ? error.message : String(error);
|
|
12806
13140
|
throw new SshDevError("AUTH_FAILED", `\u65E0\u6CD5\u8BFB\u53D6\u79C1\u94A5 ${path}\uFF1A${message}`, { path });
|
|
@@ -12832,10 +13166,8 @@ function isSsh2Available() {
|
|
|
12832
13166
|
return false;
|
|
12833
13167
|
}
|
|
12834
13168
|
}
|
|
12835
|
-
function
|
|
12836
|
-
|
|
12837
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return join9(homedir2(), path.slice(2));
|
|
12838
|
-
return path;
|
|
13169
|
+
function expandHome2(path) {
|
|
13170
|
+
return expandHome(path, homedir2());
|
|
12839
13171
|
}
|
|
12840
13172
|
var readSessionHeader = createDiskSessionHeaderReader();
|
|
12841
13173
|
|
|
@@ -12928,8 +13260,8 @@ function wireConnections(ctx, config) {
|
|
|
12928
13260
|
hostKeyPolicy: profile.hostKeyPolicy,
|
|
12929
13261
|
// A connection that names a key must use it: the alias transport otherwise relies entirely on the
|
|
12930
13262
|
// user's own ~/.ssh/config, which silently ignores the plugin's `keyPath`.
|
|
12931
|
-
...profile.auth.kind === "key" ? { identityFile:
|
|
12932
|
-
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile:
|
|
13263
|
+
...profile.auth.kind === "key" ? { identityFile: expandHome2(profile.auth.keyPath) } : {},
|
|
13264
|
+
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile: expandHome2(limits.knownHostsFile) },
|
|
12933
13265
|
// The connection's `user` must reach ssh; otherwise a bare host falls back to the local username.
|
|
12934
13266
|
...profile.user === "" ? {} : { user: profile.user },
|
|
12935
13267
|
// Password connections authenticate through an SSH_ASKPASS helper: they must stop forbidding prompts
|
|
@@ -13022,8 +13354,8 @@ function wireConnections(ctx, config) {
|
|
|
13022
13354
|
sshArgv: sshPrefix("ssh", {
|
|
13023
13355
|
alias: profile.alias ?? profile.host,
|
|
13024
13356
|
user: profile.user,
|
|
13025
|
-
...profile.auth.kind === "key" ? { identityFile:
|
|
13026
|
-
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile:
|
|
13357
|
+
...profile.auth.kind === "key" ? { identityFile: expandHome2(profile.auth.keyPath) } : {},
|
|
13358
|
+
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile: expandHome2(limits.knownHostsFile) },
|
|
13027
13359
|
hostKeyPolicy: profile.hostKeyPolicy,
|
|
13028
13360
|
connectTimeoutMs: limits.connectTimeoutMs ?? profile.connectTimeoutMs,
|
|
13029
13361
|
...passwordOptions.batchMode === void 0 ? {} : { batchMode: passwordOptions.batchMode }
|
|
@@ -13120,11 +13452,11 @@ function wireConnections(ctx, config) {
|
|
|
13120
13452
|
port: profile.port,
|
|
13121
13453
|
user: profile.user,
|
|
13122
13454
|
authKind: profile.auth.kind,
|
|
13123
|
-
...profile.auth.kind === "key" ? { identityFile:
|
|
13455
|
+
...profile.auth.kind === "key" ? { identityFile: expandHome2(profile.auth.keyPath) } : {},
|
|
13124
13456
|
connectTimeoutMs: profile.connectTimeoutMs
|
|
13125
13457
|
},
|
|
13126
13458
|
{
|
|
13127
|
-
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile:
|
|
13459
|
+
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile: expandHome2(limits.knownHostsFile) },
|
|
13128
13460
|
...limits.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: limits.connectTimeoutMs }
|
|
13129
13461
|
}
|
|
13130
13462
|
);
|
|
@@ -13254,12 +13586,8 @@ function wireConnections(ctx, config) {
|
|
|
13254
13586
|
}
|
|
13255
13587
|
function readLiveSessions(ctx) {
|
|
13256
13588
|
let list;
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
list = sessions?.list?.();
|
|
13260
|
-
} catch {
|
|
13261
|
-
return [];
|
|
13262
|
-
}
|
|
13589
|
+
const sessions = ctx.get("sessions");
|
|
13590
|
+
list = sessions?.list?.();
|
|
13263
13591
|
if (!Array.isArray(list)) return [];
|
|
13264
13592
|
const live = [];
|
|
13265
13593
|
for (const session of list) {
|
|
@@ -13277,9 +13605,13 @@ import { mkdir as mkdir7 } from "node:fs/promises";
|
|
|
13277
13605
|
// src/host/integration/remote/remote-face.ts
|
|
13278
13606
|
import { Service } from "@deepseek-ai/cordis";
|
|
13279
13607
|
import { bindTypertRemote } from "@deepseek-ai/dsh-typert-protocol";
|
|
13608
|
+
|
|
13609
|
+
// src/shared/remote-identity.ts
|
|
13280
13610
|
var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
13281
13611
|
var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
13282
|
-
var
|
|
13612
|
+
var SSH_DEV_REMOTE_METHOD = "connections";
|
|
13613
|
+
|
|
13614
|
+
// src/host/integration/remote/remote-face.ts
|
|
13283
13615
|
var NOT_CONNECTED_NOTE = "\u5C1A\u672A\u5EFA\u7ACB\u8FDE\u63A5\uFF1A\u4F20\u8F93\u51B3\u7B56\u5728\u9996\u6B21\u8FDE\u63A5\u65F6\u786E\u5B9A\u3002";
|
|
13284
13616
|
function buildConnectionStatusRows(source) {
|
|
13285
13617
|
return source.profiles().map((profile) => {
|
|
@@ -13344,7 +13676,7 @@ var SshDevRemoteFace = class extends Service {
|
|
|
13344
13676
|
return buildConnectionStatusRows(this.#source);
|
|
13345
13677
|
}
|
|
13346
13678
|
};
|
|
13347
|
-
markRemoteMethod(SshDevRemoteFace.prototype,
|
|
13679
|
+
markRemoteMethod(SshDevRemoteFace.prototype, SSH_DEV_REMOTE_METHOD);
|
|
13348
13680
|
function installRemoteFace(ctx) {
|
|
13349
13681
|
return new SshDevRemoteFace(ctx, {
|
|
13350
13682
|
profiles: () => ctx.get("sshDevConnections")?.list() ?? [],
|
|
@@ -13541,11 +13873,14 @@ function registerSessionTools(ctx, service) {
|
|
|
13541
13873
|
};
|
|
13542
13874
|
}
|
|
13543
13875
|
|
|
13544
|
-
// src/host/
|
|
13876
|
+
// src/host/integration/tools/session-directory-tools.ts
|
|
13877
|
+
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
13878
|
+
|
|
13879
|
+
// src/host/app/roots/session-directories.ts
|
|
13545
13880
|
init_errors();
|
|
13546
13881
|
|
|
13547
13882
|
// src/host/app/roots/user-roots.ts
|
|
13548
|
-
import { mkdir as mkdir4, readFile as readFile5, rename as rename3, unlink as unlink3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13883
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename3, stat as stat4, unlink as unlink3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13549
13884
|
import { join as join12 } from "node:path";
|
|
13550
13885
|
init_errors();
|
|
13551
13886
|
var USER_ROOTS_VERSION = 1;
|
|
@@ -13571,6 +13906,10 @@ var nodeUserRootsIo = {
|
|
|
13571
13906
|
await mkdir4(path, { recursive: true, mode: 448 });
|
|
13572
13907
|
},
|
|
13573
13908
|
rename: (from, to) => rename3(from, to),
|
|
13909
|
+
stat: async (path) => {
|
|
13910
|
+
const info = await stat4(path);
|
|
13911
|
+
return { size: info.size, mtimeMs: info.mtimeMs };
|
|
13912
|
+
},
|
|
13574
13913
|
unlink: async (path) => {
|
|
13575
13914
|
try {
|
|
13576
13915
|
await unlink3(path);
|
|
@@ -13933,7 +14272,19 @@ function createUserRootsStore(options) {
|
|
|
13933
14272
|
const filePath = join12(options.dir, USER_ROOTS_FILE_NAME);
|
|
13934
14273
|
let cachedRaw;
|
|
13935
14274
|
let cachedLoad;
|
|
14275
|
+
let cachedSignature;
|
|
14276
|
+
const signatureOf = async () => {
|
|
14277
|
+
if (io.stat === void 0) return void 0;
|
|
14278
|
+
try {
|
|
14279
|
+
const info = await io.stat(filePath);
|
|
14280
|
+
return `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
14281
|
+
} catch {
|
|
14282
|
+
return void 0;
|
|
14283
|
+
}
|
|
14284
|
+
};
|
|
13936
14285
|
const load = async () => {
|
|
14286
|
+
const signature = await signatureOf();
|
|
14287
|
+
if (signature !== void 0 && signature === cachedSignature && cachedLoad !== void 0) return cachedLoad;
|
|
13937
14288
|
let text8;
|
|
13938
14289
|
try {
|
|
13939
14290
|
text8 = await io.readFile(filePath);
|
|
@@ -13945,16 +14296,20 @@ function createUserRootsStore(options) {
|
|
|
13945
14296
|
degraded: true
|
|
13946
14297
|
};
|
|
13947
14298
|
}
|
|
13948
|
-
if (cachedRaw !== void 0 && text8 === cachedRaw && cachedLoad !== void 0)
|
|
14299
|
+
if (cachedRaw !== void 0 && text8 === cachedRaw && cachedLoad !== void 0) {
|
|
14300
|
+
cachedSignature = signature;
|
|
14301
|
+
return cachedLoad;
|
|
14302
|
+
}
|
|
13949
14303
|
const parsed = parseUserRoots(text8);
|
|
13950
14304
|
cachedRaw = text8;
|
|
13951
14305
|
cachedLoad = parsed;
|
|
14306
|
+
cachedSignature = signature;
|
|
13952
14307
|
return parsed;
|
|
13953
14308
|
};
|
|
13954
|
-
const write = async (next) => {
|
|
14309
|
+
const write = async (next, preparedText) => {
|
|
13955
14310
|
const validated = validateState(next);
|
|
13956
14311
|
const temporary = `${filePath}.${process.pid}-${++tempSequence2}.tmp`;
|
|
13957
|
-
const text8 = serializeUserRoots(validated);
|
|
14312
|
+
const text8 = preparedText ?? serializeUserRoots(validated);
|
|
13958
14313
|
await io.mkdir(options.dir);
|
|
13959
14314
|
try {
|
|
13960
14315
|
await io.writeFile(temporary, text8);
|
|
@@ -13965,6 +14320,7 @@ function createUserRootsStore(options) {
|
|
|
13965
14320
|
}
|
|
13966
14321
|
cachedRaw = text8;
|
|
13967
14322
|
cachedLoad = { state: validated, warnings: [], degraded: false };
|
|
14323
|
+
cachedSignature = void 0;
|
|
13968
14324
|
return validated;
|
|
13969
14325
|
};
|
|
13970
14326
|
const queue = createSerialQueue();
|
|
@@ -13974,16 +14330,18 @@ function createUserRootsStore(options) {
|
|
|
13974
14330
|
assertUserRootsWritable(await load());
|
|
13975
14331
|
return write(next);
|
|
13976
14332
|
});
|
|
14333
|
+
const applyTransition = async (transition) => {
|
|
14334
|
+
const loaded = await load();
|
|
14335
|
+
assertUserRootsWritable(loaded);
|
|
14336
|
+
const wanted = transition(loaded.state);
|
|
14337
|
+
if (wanted === loaded.state) return loaded.state;
|
|
14338
|
+
const wantedText = serializeUserRoots(wanted);
|
|
14339
|
+
if (cachedRaw !== void 0 && wantedText === cachedRaw) return loaded.state;
|
|
14340
|
+
return write(wanted, wantedText);
|
|
14341
|
+
};
|
|
13977
14342
|
const mutate = (transition) => {
|
|
13978
14343
|
const locked = options.withLock;
|
|
13979
|
-
|
|
13980
|
-
const loaded = await load();
|
|
13981
|
-
assertUserRootsWritable(loaded);
|
|
13982
|
-
const wanted = transition(loaded.state);
|
|
13983
|
-
if (serializeUserRoots(wanted) === serializeUserRoots(loaded.state)) return loaded.state;
|
|
13984
|
-
return write(wanted);
|
|
13985
|
-
});
|
|
13986
|
-
return locked === void 0 ? run() : locked(run);
|
|
14344
|
+
return serialize(() => locked === void 0 ? applyTransition(transition) : locked(() => applyTransition(transition)));
|
|
13987
14345
|
};
|
|
13988
14346
|
return {
|
|
13989
14347
|
filePath,
|
|
@@ -14137,6 +14495,7 @@ function isMissingFile(error) {
|
|
|
14137
14495
|
}
|
|
14138
14496
|
|
|
14139
14497
|
// src/host/app/roots/roots-api-http.ts
|
|
14498
|
+
init_errors();
|
|
14140
14499
|
async function readBody(req, maxBytes) {
|
|
14141
14500
|
const chunks = [];
|
|
14142
14501
|
let total = 0;
|
|
@@ -14193,11 +14552,17 @@ var STATUS_BY_CODE = {
|
|
|
14193
14552
|
ROOT_INVALID: 400,
|
|
14194
14553
|
CONNECTION_INVALID: 400,
|
|
14195
14554
|
CONNECTION_NOT_FOUND: 400,
|
|
14555
|
+
// 路径不在任何已注册的根内 / 根没有注册:客户端换一个路径或先加根就能成功,不是服务器故障。
|
|
14556
|
+
// 它此前漏在表外,于是**所有**经 HTTP 暴露的根外访问都报 500(审计 2026-11-06 第 4 轮 P1)。
|
|
14557
|
+
ROOT_OUTSIDE: 400,
|
|
14196
14558
|
// 目标不存在 / 状态冲突 / 本组合不支持:客户端可以据此换一条路。
|
|
14197
14559
|
TRASH_NOT_FOUND: 404,
|
|
14198
14560
|
TRANSFER_CONFLICT: 409,
|
|
14199
14561
|
SESSION_ACTIVE: 409,
|
|
14200
|
-
REMOTE_UNSUPPORTED: 501
|
|
14562
|
+
REMOTE_UNSUPPORTED: 501,
|
|
14563
|
+
// 调用方主动取消(客户端断开/关掉面板)。499 是 nginx 的 "client closed request" 约定:它不是一个错误,
|
|
14564
|
+
// 只是说明"没人等这个回答了";映射成 500 会让日志里出现一堆假故障。
|
|
14565
|
+
CANCELLED: 499
|
|
14201
14566
|
};
|
|
14202
14567
|
function codeOf(error) {
|
|
14203
14568
|
const code = asRecord2(error)?.["code"];
|
|
@@ -14232,6 +14597,9 @@ function optionalPath(value) {
|
|
|
14232
14597
|
reason: "invalid-path"
|
|
14233
14598
|
});
|
|
14234
14599
|
}
|
|
14600
|
+
if (/[\u0000-\u001f]/.test(value)) {
|
|
14601
|
+
throw new SshDevError("ROOT_INVALID", "path \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\u542B NUL\uFF09", { reason: "invalid-path" });
|
|
14602
|
+
}
|
|
14235
14603
|
return value.trim();
|
|
14236
14604
|
}
|
|
14237
14605
|
function pick(source, fields) {
|
|
@@ -14275,27 +14643,375 @@ function connectionIdFromAlias(alias) {
|
|
|
14275
14643
|
return isValidConnectionId(folded) ? folded : void 0;
|
|
14276
14644
|
}
|
|
14277
14645
|
|
|
14278
|
-
// src/
|
|
14279
|
-
var
|
|
14280
|
-
var
|
|
14281
|
-
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14646
|
+
// src/host/app/roots/session-directories.ts
|
|
14647
|
+
var SESSION_DIRECTORY_MAX_ROOTS = 32;
|
|
14648
|
+
var SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14649
|
+
var REMOTE_SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14650
|
+
var REMOTE_BROAD_ROOTS = [...REMOTE_SYSTEM_TREES, "/home", "/opt", "/srv", "/mnt", "/media", "/tmp"];
|
|
14651
|
+
function remotePathTooBroad(normalized) {
|
|
14652
|
+
if (REMOTE_BROAD_ROOTS.includes(normalized)) return true;
|
|
14653
|
+
for (const tree of REMOTE_SYSTEM_TREES) {
|
|
14654
|
+
if (normalized === tree || normalized.startsWith(`${tree}/`)) return true;
|
|
14655
|
+
}
|
|
14656
|
+
return false;
|
|
14285
14657
|
}
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14658
|
+
function directoryRefusal(request) {
|
|
14659
|
+
const trimmed = request.path.trim();
|
|
14660
|
+
if (trimmed === "") return "\u76EE\u5F55\u8DEF\u5F84\u4E3A\u7A7A\u3002";
|
|
14661
|
+
if (request.location === "remote") {
|
|
14662
|
+
if (request.connectionId === void 0 || request.connectionId === "") {
|
|
14663
|
+
return "\u8FDC\u7AEF\u76EE\u5F55\u9700\u8981 connectionId\uFF08\u5148\u786E\u8BA4\u76EE\u6807\u8FDE\u63A5\uFF09\u3002";
|
|
14664
|
+
}
|
|
14665
|
+
if (!trimmed.startsWith("/")) return `\u8FDC\u7AEF\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${trimmed}`;
|
|
14666
|
+
let normalized2;
|
|
14667
|
+
try {
|
|
14668
|
+
normalized2 = normalizeRemotePath(trimmed);
|
|
14669
|
+
} catch (error) {
|
|
14670
|
+
return error instanceof Error ? error.message : `\u8FDC\u7AEF\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF1A${trimmed}`;
|
|
14671
|
+
}
|
|
14672
|
+
if (normalized2 === "/") return "\u4E0D\u5141\u8BB8\u628A\u8FDC\u7AEF\u7684\u6839\u76EE\u5F55 / \u6DFB\u52A0\u5230\u4F1A\u8BDD\u3002";
|
|
14673
|
+
if (remotePathTooBroad(normalized2)) {
|
|
14674
|
+
return `\u4E0D\u5141\u8BB8\u628A\u8FC7\u5BBD\u7684\u8FDC\u7AEF\u76EE\u5F55\u6DFB\u52A0\u5230\u4F1A\u8BDD\uFF08${normalized2}\uFF09\uFF1A\u8BF7\u9009\u5230\u5177\u4F53\u9879\u76EE\u76EE\u5F55\u3002`;
|
|
14675
|
+
}
|
|
14676
|
+
return void 0;
|
|
14677
|
+
}
|
|
14678
|
+
const normalized = trimmed.replace(/\/+$/u, "") || "/";
|
|
14679
|
+
if (!isAbsoluteLocalPath(normalized)) return `\u672C\u5730\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${trimmed}`;
|
|
14680
|
+
if (normalized === "/") return "\u4E0D\u5141\u8BB8\u628A\u672C\u673A\u7684\u6839\u76EE\u5F55 / \u6DFB\u52A0\u5230\u4F1A\u8BDD\u3002";
|
|
14681
|
+
if (SYSTEM_TREES.some((tree) => normalized === tree || localPathContains(tree, normalized))) {
|
|
14682
|
+
return `\u4E0D\u5141\u8BB8\u628A\u7CFB\u7EDF\u76EE\u5F55\u6DFB\u52A0\u5230\u4F1A\u8BDD\uFF1A${normalized}`;
|
|
14683
|
+
}
|
|
14684
|
+
const workspace = request.workspacePath.trim();
|
|
14685
|
+
if (workspace !== "") {
|
|
14686
|
+
if (normalized === workspace || localPathContains(normalized, workspace)) {
|
|
14687
|
+
return `\u4E0D\u5141\u8BB8\u6DFB\u52A0\u5F53\u524D\u5DE5\u4F5C\u533A\u7684\u7956\u5148\u76EE\u5F55\uFF08${normalized}\uFF09\uFF1A\u90A3\u4F1A\u628A\u6574\u68F5\u7236\u6811\u7EB3\u5165\u53EF\u5199\u8303\u56F4\u3002\u8BF7\u6DFB\u52A0\u5DE5\u4F5C\u533A\u5185\u90E8\u6216\u4E0E\u5B83\u5E76\u5217\u7684\u76EE\u5F55\u3002`;
|
|
14688
|
+
}
|
|
14689
|
+
}
|
|
14690
|
+
return void 0;
|
|
14691
|
+
}
|
|
14692
|
+
function createSessionDirectories(deps) {
|
|
14693
|
+
return {
|
|
14694
|
+
async add(request) {
|
|
14695
|
+
const refusal = directoryRefusal(request);
|
|
14696
|
+
if (refusal !== void 0) return { ok: false, reason: refusal };
|
|
14697
|
+
if (request.workspacePath.trim() === "") return { ok: false, reason: "\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u6DFB\u52A0\u76EE\u5F55\u3002" };
|
|
14698
|
+
if (request.sessionId.trim() === "") return { ok: false, reason: "\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u6DFB\u52A0\u76EE\u5F55\u3002" };
|
|
14699
|
+
const entry = {
|
|
14700
|
+
location: request.location,
|
|
14701
|
+
path: request.path.trim(),
|
|
14702
|
+
...request.connectionId === void 0 ? {} : { connectionId: request.connectionId },
|
|
14703
|
+
writable: true,
|
|
14704
|
+
workspaces: [request.workspacePath],
|
|
14705
|
+
sessions: [request.sessionId]
|
|
14706
|
+
};
|
|
14707
|
+
const key = userRootKey(entry);
|
|
14708
|
+
let state;
|
|
14709
|
+
try {
|
|
14710
|
+
state = await deps.mutateUserState((current) => {
|
|
14711
|
+
const existing = findRootInState(current, key);
|
|
14712
|
+
if (existing === void 0) {
|
|
14713
|
+
const scoped = current.roots.filter(
|
|
14714
|
+
(candidate) => (candidate.sessions ?? []).includes(request.sessionId) || (candidate.workspaces ?? []).includes(request.workspacePath)
|
|
14715
|
+
).length;
|
|
14716
|
+
if (scoped >= SESSION_DIRECTORY_MAX_ROOTS) {
|
|
14717
|
+
throw new SshDevError(
|
|
14718
|
+
"ROOT_INVALID",
|
|
14719
|
+
`\u672C\u4F1A\u8BDD\u5DF2\u6DFB\u52A0 ${scoped} \u4E2A\u76EE\u5F55\uFF0C\u8FBE\u5230\u4E0A\u9650 ${SESSION_DIRECTORY_MAX_ROOTS}\uFF1B\u8BF7\u5148\u79FB\u9664\u4E0D\u518D\u9700\u8981\u7684\u76EE\u5F55\u3002`,
|
|
14720
|
+
{ limit: SESSION_DIRECTORY_MAX_ROOTS }
|
|
14721
|
+
);
|
|
14722
|
+
}
|
|
14723
|
+
return addRootToState(current, entry);
|
|
14724
|
+
}
|
|
14725
|
+
return withSessions(withWorkspaces(current, key, [request.workspacePath]), key, [request.sessionId]);
|
|
14726
|
+
});
|
|
14727
|
+
} catch (error) {
|
|
14728
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
14729
|
+
}
|
|
14730
|
+
return { ok: true, key, ...await ensureLiveAndReadAnchor(deps, key, state) };
|
|
14731
|
+
},
|
|
14732
|
+
async remove(request) {
|
|
14733
|
+
if (request.sessionId.trim() === "") return { ok: false, reason: "\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u79FB\u9664\u76EE\u5F55" };
|
|
14734
|
+
let key = request.key?.trim() ?? "";
|
|
14735
|
+
let before;
|
|
14736
|
+
let state;
|
|
14737
|
+
try {
|
|
14738
|
+
state = await deps.mutateUserState((current) => {
|
|
14739
|
+
const resolved = resolveRemoveKey(current, request);
|
|
14740
|
+
if (resolved === void 0) {
|
|
14741
|
+
throw new SshDevError(
|
|
14742
|
+
"ROOT_INVALID",
|
|
14743
|
+
request.path === void 0 ? `\u6CA1\u6709\u627E\u5230\u8981\u79FB\u9664\u7684\u76EE\u5F55\uFF1A${key}` : `\u672C\u4F1A\u8BDD\u6CA1\u6709\u8FD9\u4E2A\u76EE\u5F55\uFF1A${request.path}`,
|
|
14744
|
+
{ key, path: request.path }
|
|
14745
|
+
);
|
|
14746
|
+
}
|
|
14747
|
+
key = resolved.key;
|
|
14748
|
+
before = resolved.entry;
|
|
14749
|
+
if (!(resolved.entry.sessions ?? []).includes(request.sessionId)) {
|
|
14750
|
+
throw new SshDevError("ROOT_INVALID", "\u53EA\u80FD\u79FB\u9664\u672C\u4F1A\u8BDD\u81EA\u5DF1\u6DFB\u52A0\u7684\u76EE\u5F55\uFF1B\u8FD9\u6761\u4E0D\u662F\u672C\u4F1A\u8BDD\u52A0\u7684\u3002", { key });
|
|
14751
|
+
}
|
|
14752
|
+
return removeRootFromSession(current, key, request.sessionId);
|
|
14753
|
+
});
|
|
14754
|
+
} catch (error) {
|
|
14755
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
14756
|
+
}
|
|
14757
|
+
if (before === void 0) return { ok: false, reason: `\u6CA1\u6709\u627E\u5230\u8981\u79FB\u9664\u7684\u76EE\u5F55\uFF1A${key}` };
|
|
14758
|
+
if (!stateHasRoot(state, key)) {
|
|
14759
|
+
const liveKey = await findLiveKey(deps, before);
|
|
14760
|
+
if (liveKey !== void 0) await deps.unregisterRoot(liveKey);
|
|
14761
|
+
return { ok: true, key };
|
|
14762
|
+
}
|
|
14763
|
+
await refreshLiveScope(deps, key, state);
|
|
14764
|
+
return { ok: true, key };
|
|
14765
|
+
}
|
|
14766
|
+
};
|
|
14767
|
+
}
|
|
14768
|
+
function resolveRemoveKey(state, request) {
|
|
14769
|
+
const wantedKey = request.key?.trim();
|
|
14770
|
+
if (wantedKey !== void 0 && wantedKey !== "") {
|
|
14771
|
+
const entry = findRootInState(state, wantedKey);
|
|
14772
|
+
return entry === void 0 ? void 0 : { key: wantedKey, entry };
|
|
14773
|
+
}
|
|
14774
|
+
const path = request.path?.trim();
|
|
14775
|
+
if (path === void 0 || path === "") return void 0;
|
|
14776
|
+
const wanted = request.connectionId === void 0 ? userRootKey({ location: "local", path }) : userRootKey({ location: "remote", path, connectionId: request.connectionId });
|
|
14777
|
+
for (const entry of state.roots) {
|
|
14778
|
+
if (userRootKey(entry) === wanted) return { key: wanted, entry };
|
|
14779
|
+
}
|
|
14780
|
+
if (request.connectionId === void 0) {
|
|
14781
|
+
const matches = state.roots.filter((entry) => entry.path === path);
|
|
14782
|
+
if (matches.length === 1) return { key: userRootKey(matches[0]), entry: matches[0] };
|
|
14783
|
+
}
|
|
14784
|
+
return void 0;
|
|
14785
|
+
}
|
|
14786
|
+
async function ensureLiveAndReadAnchor(deps, key, state) {
|
|
14787
|
+
const persisted = findRootInState(state, key);
|
|
14788
|
+
if (persisted === void 0) return {};
|
|
14789
|
+
const readAnchor = (roots2) => {
|
|
14790
|
+
for (const raw of roots2) {
|
|
14791
|
+
if (liveKeyOfRecord(raw, persisted) === void 0) continue;
|
|
14792
|
+
const source = raw?.root ?? raw;
|
|
14793
|
+
const anchor = source["anchorPath"];
|
|
14794
|
+
if (typeof anchor === "string" && anchor !== "") return { anchorPath: anchor };
|
|
14795
|
+
return {};
|
|
14796
|
+
}
|
|
14797
|
+
return {};
|
|
14798
|
+
};
|
|
14799
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14800
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14801
|
+
if (liveEntry === void 0) {
|
|
14802
|
+
await deps.registerRoot(persisted);
|
|
14803
|
+
return readAnchor(await Promise.resolve(deps.listRoots()));
|
|
14804
|
+
}
|
|
14805
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return readAnchor(roots);
|
|
14806
|
+
const liveKey = liveKeyOfRecord(liveEntry, persisted);
|
|
14807
|
+
if (liveKey !== void 0) {
|
|
14808
|
+
await deps.unregisterRoot(liveKey);
|
|
14809
|
+
await deps.registerRoot(persisted);
|
|
14810
|
+
}
|
|
14811
|
+
return readAnchor(await Promise.resolve(deps.listRoots()));
|
|
14812
|
+
}
|
|
14813
|
+
async function liveScopeWarning(deps, key, state) {
|
|
14814
|
+
const persisted = findRootInState(state, key);
|
|
14815
|
+
if (persisted === void 0) return void 0;
|
|
14816
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14817
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14818
|
+
if (liveEntry === void 0) return `\u6DFB\u52A0\u76EE\u5F55\uFF1A\u6839 ${key} \u4E0D\u5728\u5B9E\u65F6\u6CE8\u518C\u8868\u91CC\uFF0C\u91CD\u542F\u524D\u53EF\u80FD\u4E0D\u53EF\u7528`;
|
|
14819
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return void 0;
|
|
14820
|
+
return `\u6DFB\u52A0\u76EE\u5F55\uFF1A\u6839 ${key} \u7684\u5B9E\u65F6\u4F5C\u7528\u57DF\u4E0E\u6587\u4EF6\u4E0D\u4E00\u81F4\uFF0C\u53E6\u4E00\u4E2A\u4F1A\u8BDD\u53EF\u80FD\u8981\u91CD\u542F\u540E\u624D\u53EF\u89C1`;
|
|
14821
|
+
}
|
|
14822
|
+
async function findLiveKey(deps, entry) {
|
|
14823
|
+
for (const raw of await Promise.resolve(deps.listRoots())) {
|
|
14824
|
+
const key = liveKeyOfRecord(raw, entry);
|
|
14825
|
+
if (key !== void 0) return key;
|
|
14826
|
+
}
|
|
14827
|
+
return void 0;
|
|
14828
|
+
}
|
|
14829
|
+
async function refreshLiveScope(deps, key, state) {
|
|
14830
|
+
const persisted = findRootInState(state, key);
|
|
14831
|
+
if (persisted === void 0) return;
|
|
14832
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14833
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14834
|
+
if (liveEntry === void 0) return;
|
|
14835
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return;
|
|
14836
|
+
const liveKey = liveKeyOfRecord(liveEntry, persisted);
|
|
14837
|
+
if (liveKey === void 0) return;
|
|
14838
|
+
await deps.unregisterRoot(liveKey);
|
|
14839
|
+
await deps.registerRoot(persisted);
|
|
14840
|
+
}
|
|
14841
|
+
function liveScopeIsCurrent(raw, entry) {
|
|
14842
|
+
const outer = raw ?? {};
|
|
14843
|
+
const source = outer.root ?? outer;
|
|
14844
|
+
if (source["shared"] === true) return true;
|
|
14845
|
+
return sameStrings(source["workspaces"], entry.workspaces) && sameStrings(source["sessions"], entry.sessions);
|
|
14846
|
+
}
|
|
14847
|
+
function sameStrings(left, right) {
|
|
14848
|
+
const asSet = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").sort() : [];
|
|
14849
|
+
const a = asSet(left);
|
|
14850
|
+
const b = asSet(right);
|
|
14851
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
14852
|
+
}
|
|
14853
|
+
|
|
14854
|
+
// src/host/integration/tools/session-directory-tools.ts
|
|
14855
|
+
var SESSION_DIRECTORY_TOOL_NAMES = {
|
|
14856
|
+
// **必须以 `remote_` 开头**:插件的严格参数校验网关按这个名字前缀覆盖它的全部工具
|
|
14857
|
+
//(`strict-arguments.ts` 的 `STRICT_TOOL_PREFIX`),换个前缀就会静默逃过参数校验。
|
|
14858
|
+
add: "remote_add_directory",
|
|
14859
|
+
remove: "remote_remove_directory"
|
|
14860
|
+
};
|
|
14861
|
+
function sessionDirectoryToolDefinitions(deps) {
|
|
14862
|
+
return [
|
|
14863
|
+
defineTool2({
|
|
14864
|
+
name: SESSION_DIRECTORY_TOOL_NAMES.add,
|
|
14865
|
+
description: `Add a directory to **this conversation**, so its file tools and \`@\` completion can use it. The directory becomes part of the session workspace: under \`workspace-write\` it is readable and writable, and it is visible to this conversation only (the user's card lists it). **Requires human approval** \u2014 the call is refused when the deployment has no approval surface. Refused: a relative path, the machine root \`/\`, a system tree, a directory that is the current workspace **or one of its ancestors** (that would hand the model the whole parent tree), and more than ${SESSION_DIRECTORY_MAX_ROOTS} directories in one conversation. Prefer asking the user to add a directory when you are unsure: this tool changes your own permission boundary.`,
|
|
14866
|
+
parameters: {
|
|
14867
|
+
path: {
|
|
14868
|
+
type: "string",
|
|
14869
|
+
description: "Absolute path. Local spelling for a local directory, absolute remote path when `connectionId` is given.",
|
|
14870
|
+
required: true
|
|
14871
|
+
},
|
|
14872
|
+
connectionId: {
|
|
14873
|
+
type: "string",
|
|
14874
|
+
description: "Configured connection id when the directory is on a remote host. Omit for a local directory."
|
|
14875
|
+
}
|
|
14876
|
+
},
|
|
14877
|
+
output: {
|
|
14878
|
+
schema: {
|
|
14879
|
+
type: "object",
|
|
14880
|
+
additionalProperties: false,
|
|
14881
|
+
properties: {
|
|
14882
|
+
path: { type: "string", required: true },
|
|
14883
|
+
added: { type: "boolean", required: true },
|
|
14884
|
+
anchorPath: { type: "string" },
|
|
14885
|
+
note: { type: "string" }
|
|
14886
|
+
}
|
|
14887
|
+
},
|
|
14888
|
+
render: (_args, value) => {
|
|
14889
|
+
const record2 = value;
|
|
14890
|
+
const anchor = typeof record2["anchorPath"] === "string" ? `\uFF08\u951A\u70B9 ${record2["anchorPath"]}\uFF09` : "";
|
|
14891
|
+
return [{ type: "text", text: `\u5DF2\u52A0\u5165\u672C\u4F1A\u8BDD\uFF1A${String(record2["path"] ?? "")}${anchor}\u3002` }];
|
|
14892
|
+
}
|
|
14893
|
+
},
|
|
14894
|
+
async execute(args, exec) {
|
|
14895
|
+
const record2 = args ?? {};
|
|
14896
|
+
const path = typeof record2["path"] === "string" ? record2["path"].trim() : "";
|
|
14897
|
+
if (path === "") throw new Error("path \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
14898
|
+
const sessionId = deps.sessionId();
|
|
14899
|
+
if (sessionId === void 0 || sessionId === "") throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u6DFB\u52A0\u76EE\u5F55");
|
|
14900
|
+
const workspacePath = deps.workspacePathOf(sessionId);
|
|
14901
|
+
if (workspacePath === void 0 || workspacePath === "") {
|
|
14902
|
+
throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u628A\u76EE\u5F55\u52A0\u5165\u672C\u4F1A\u8BDD");
|
|
14903
|
+
}
|
|
14904
|
+
const connectionId = typeof record2["connectionId"] === "string" && record2["connectionId"] !== "" ? record2["connectionId"] : void 0;
|
|
14905
|
+
const outcome = await deps.directories.add({
|
|
14906
|
+
location: connectionId === void 0 ? "local" : "remote",
|
|
14907
|
+
path,
|
|
14908
|
+
...connectionId === void 0 ? {} : { connectionId },
|
|
14909
|
+
workspacePath,
|
|
14910
|
+
sessionId
|
|
14911
|
+
});
|
|
14912
|
+
if (!outcome.ok) throw new Error(outcome.reason);
|
|
14913
|
+
return {
|
|
14914
|
+
path,
|
|
14915
|
+
added: true,
|
|
14916
|
+
...outcome.anchorPath === void 0 ? {} : { anchorPath: outcome.anchorPath },
|
|
14917
|
+
note: "\u5DF2\u52A0\u5165\u672C\u4F1A\u8BDD\uFF1B`workspace-write` \u4E0B\u8BE5\u76EE\u5F55\u53EF\u8BFB\u53EF\u5199\u3002"
|
|
14918
|
+
};
|
|
14919
|
+
}
|
|
14920
|
+
}),
|
|
14921
|
+
defineTool2({
|
|
14922
|
+
name: SESSION_DIRECTORY_TOOL_NAMES.remove,
|
|
14923
|
+
description: "Remove a directory that **this conversation added** from its list. The directory stays registered for any other conversation that added it; a directory the user added in the plugin settings, and the conversation workspace itself, cannot be removed here. Removing only narrows access, so it needs no approval.",
|
|
14924
|
+
parameters: {
|
|
14925
|
+
path: { type: "string", description: "Absolute path of the directory to remove, as it was added.", required: true },
|
|
14926
|
+
connectionId: {
|
|
14927
|
+
type: "string",
|
|
14928
|
+
description: "Connection id when the directory is remote; must match the one it was added with."
|
|
14929
|
+
}
|
|
14930
|
+
},
|
|
14931
|
+
output: {
|
|
14932
|
+
schema: {
|
|
14933
|
+
type: "object",
|
|
14934
|
+
additionalProperties: false,
|
|
14935
|
+
properties: {
|
|
14936
|
+
path: { type: "string", required: true },
|
|
14937
|
+
removed: { type: "boolean", required: true },
|
|
14938
|
+
note: { type: "string" }
|
|
14939
|
+
}
|
|
14940
|
+
},
|
|
14941
|
+
render: (_args, value) => {
|
|
14942
|
+
const record2 = value;
|
|
14943
|
+
return [{ type: "text", text: `\u5DF2\u4ECE\u672C\u4F1A\u8BDD\u79FB\u9664\uFF1A${String(record2["path"] ?? "")}\u3002` }];
|
|
14944
|
+
}
|
|
14945
|
+
},
|
|
14946
|
+
async execute(args) {
|
|
14947
|
+
const record2 = args ?? {};
|
|
14948
|
+
const path = typeof record2["path"] === "string" ? record2["path"].trim() : "";
|
|
14949
|
+
if (path === "") throw new Error("path \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
14950
|
+
const sessionId = deps.sessionId();
|
|
14951
|
+
if (sessionId === void 0 || sessionId === "") throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u79FB\u9664\u76EE\u5F55");
|
|
14952
|
+
const workspacePath = deps.workspacePathOf(sessionId);
|
|
14953
|
+
if (workspacePath === void 0 || workspacePath === "") {
|
|
14954
|
+
throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u79FB\u9664\u76EE\u5F55");
|
|
14955
|
+
}
|
|
14956
|
+
const connectionId = typeof record2["connectionId"] === "string" && record2["connectionId"] !== "" ? record2["connectionId"] : void 0;
|
|
14957
|
+
const outcome = await deps.directories.remove({
|
|
14958
|
+
path,
|
|
14959
|
+
...connectionId === void 0 ? {} : { connectionId },
|
|
14960
|
+
workspacePath,
|
|
14961
|
+
sessionId
|
|
14962
|
+
});
|
|
14963
|
+
if (!outcome.ok) throw new Error(outcome.reason);
|
|
14964
|
+
return { path, removed: true, note: "\u5DF2\u4ECE\u672C\u4F1A\u8BDD\u79FB\u9664\u3002" };
|
|
14965
|
+
}
|
|
14966
|
+
})
|
|
14967
|
+
];
|
|
14968
|
+
}
|
|
14969
|
+
function sessionDirectoryApprovalGate(ctx) {
|
|
14970
|
+
return ctx.on("tools/pre-execute", async (exec, next) => {
|
|
14971
|
+
if (exec.name !== SESSION_DIRECTORY_TOOL_NAMES.add) return next();
|
|
14972
|
+
const args = exec.arguments ?? {};
|
|
14973
|
+
const target = String(args["path"] ?? "");
|
|
14974
|
+
const where = typeof args["connectionId"] === "string" && args["connectionId"] !== "" ? `${args["connectionId"]}:${target}` : target;
|
|
14975
|
+
return {
|
|
14976
|
+
kind: "ask",
|
|
14977
|
+
reason: `${SESSION_DIRECTORY_TOOL_NAMES.add} \u60F3\u628A ${where} \u52A0\u5165\u672C\u4F1A\u8BDD\uFF08\u4F1A\u6269\u5927\u6A21\u578B\u53EF\u8BFB\u5199\u7684\u8303\u56F4\uFF09`,
|
|
14978
|
+
displayReason: {
|
|
14979
|
+
en: `Add ${where} to this conversation? It becomes readable and writable by the model.`,
|
|
14980
|
+
zh: `\u628A ${where} \u52A0\u5165\u672C\u4F1A\u8BDD\uFF1F\u52A0\u5165\u540E\u6A21\u578B\u53EF\u8BFB\u5199\u8BE5\u76EE\u5F55\u3002`
|
|
14981
|
+
}
|
|
14982
|
+
};
|
|
14983
|
+
});
|
|
14984
|
+
}
|
|
14985
|
+
function registerSessionDirectoryTools(ctx, deps) {
|
|
14986
|
+
const disposers = sessionDirectoryToolDefinitions(deps).map((definition) => ctx.tools.register(definition));
|
|
14987
|
+
const disposeGate = sessionDirectoryApprovalGate(ctx);
|
|
14988
|
+
return () => {
|
|
14989
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
14990
|
+
disposeGate();
|
|
14991
|
+
};
|
|
14992
|
+
}
|
|
14993
|
+
|
|
14994
|
+
// src/shared/routes.ts
|
|
14995
|
+
var ROOTS_API_PATH = "/api/over-ssh/roots";
|
|
14996
|
+
var SESSION_API_PATH = "/api/over-ssh/sessions";
|
|
14997
|
+
function asRouteHost(value) {
|
|
14998
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
14999
|
+
const register = value.register;
|
|
15000
|
+
return typeof register === "function" ? value : void 0;
|
|
15001
|
+
}
|
|
15002
|
+
|
|
15003
|
+
// src/host/app/roots/roots-api-contract.ts
|
|
15004
|
+
var ROOTS_API_MAX_BODY_BYTES = 64 * 1024;
|
|
15005
|
+
var DIRECTORY_LIST_MAX_ENTRIES = 2e3;
|
|
15006
|
+
var REMOTE_LIST_MAX_ENTRIES = DIRECTORY_LIST_MAX_ENTRIES;
|
|
15007
|
+
var LOCAL_LIST_MAX_ENTRIES = DIRECTORY_LIST_MAX_ENTRIES;
|
|
15008
|
+
var ROOTS_API_ACTIONS = [
|
|
15009
|
+
"add-root",
|
|
15010
|
+
"remove-root",
|
|
15011
|
+
"add-connection",
|
|
15012
|
+
"update-connection",
|
|
15013
|
+
"test-connection",
|
|
15014
|
+
"remove-connection",
|
|
14299
15015
|
"list-remote",
|
|
14300
15016
|
"list-local",
|
|
14301
15017
|
"list-ssh-config",
|
|
@@ -14369,9 +15085,25 @@ function publicRoot(raw) {
|
|
|
14369
15085
|
...typeof source["connectionId"] === "string" ? { connectionId: source["connectionId"] } : {},
|
|
14370
15086
|
path: String(source["path"] ?? ""),
|
|
14371
15087
|
...typeof source["label"] === "string" ? { label: source["label"] } : {},
|
|
14372
|
-
...typeof source["writable"] === "boolean" ? { writable: source["writable"] } : {}
|
|
15088
|
+
...typeof source["writable"] === "boolean" ? { writable: source["writable"] } : {},
|
|
15089
|
+
...stringList(source["workspaces"]) === void 0 ? {} : { workspaces: stringList(source["workspaces"]) },
|
|
15090
|
+
...stringList(source["sessions"]) === void 0 ? {} : { sessions: stringList(source["sessions"]) }
|
|
14373
15091
|
};
|
|
14374
15092
|
}
|
|
15093
|
+
function liveRootScopes(raw) {
|
|
15094
|
+
const outer = asRecord2(raw) ?? {};
|
|
15095
|
+
const source = asRecord2(outer["root"]) ?? outer;
|
|
15096
|
+
const sessions = stringList(source["sessions"]);
|
|
15097
|
+
const workspaces = stringList(source["workspaces"]);
|
|
15098
|
+
return {
|
|
15099
|
+
...sessions === void 0 ? {} : { sessions },
|
|
15100
|
+
...workspaces === void 0 ? {} : { workspaces }
|
|
15101
|
+
};
|
|
15102
|
+
}
|
|
15103
|
+
function stringList(value) {
|
|
15104
|
+
if (!Array.isArray(value)) return void 0;
|
|
15105
|
+
return value.filter((item) => typeof item === "string");
|
|
15106
|
+
}
|
|
14375
15107
|
function publicConnection(raw) {
|
|
14376
15108
|
const source = asRecord2(raw) ?? {};
|
|
14377
15109
|
const authKind = source["authKind"] ?? asRecord2(source["auth"])?.["kind"];
|
|
@@ -14497,21 +15229,34 @@ async function handleRootActions(action, body, req, res, deps) {
|
|
|
14497
15229
|
if (registeredNow) await deps.unregisterRoot(key);
|
|
14498
15230
|
throw error;
|
|
14499
15231
|
}
|
|
15232
|
+
const warnings = [];
|
|
15233
|
+
if (existing !== void 0) {
|
|
15234
|
+
const warning = await liveScopeWarning(deps, key, state);
|
|
15235
|
+
if (warning !== void 0) warnings.push(warning);
|
|
15236
|
+
}
|
|
14500
15237
|
const anchorPath = await anchorPathOf(deps, entry);
|
|
14501
15238
|
send(req, res, 200, {
|
|
14502
15239
|
ok: true,
|
|
14503
15240
|
action,
|
|
14504
15241
|
state: publicUserState(state),
|
|
14505
|
-
...anchorPath === void 0 ? {} : { anchorPath }
|
|
15242
|
+
...anchorPath === void 0 ? {} : { anchorPath },
|
|
15243
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
14506
15244
|
});
|
|
14507
15245
|
return true;
|
|
14508
|
-
return true;
|
|
14509
15246
|
});
|
|
14510
15247
|
}
|
|
14511
15248
|
case "remove-root": {
|
|
14512
15249
|
const key = requiredKey(body["key"]);
|
|
14513
15250
|
const workspace = optionalWorkspaceName(body["workspace"]);
|
|
14514
15251
|
const session = optionalSessionName(body["session"]);
|
|
15252
|
+
if (session !== void 0) {
|
|
15253
|
+
const current = await currentUserState(deps);
|
|
15254
|
+
const entry = findRootInState(current, key);
|
|
15255
|
+
const owners = entry?.sessions ?? [];
|
|
15256
|
+
if (owners.length > 0 && !owners.includes(session)) {
|
|
15257
|
+
throw new SshDevError("ROOT_INVALID", "\u8FD9\u4E2A\u76EE\u5F55\u662F\u53E6\u4E00\u4E2A\u4F1A\u8BDD\u6DFB\u52A0\u7684\uFF1B\u8BF7\u5230\u6DFB\u52A0\u5B83\u7684\u4F1A\u8BDD\u91CC\u79FB\u9664", { key });
|
|
15258
|
+
}
|
|
15259
|
+
}
|
|
14515
15260
|
const state = await reconcile(deps, (current) => {
|
|
14516
15261
|
if (session !== void 0 && rootAddedBySession(current, key, session)) {
|
|
14517
15262
|
return removeRootFromSession(current, key, session);
|
|
@@ -14529,30 +15274,6 @@ async function handleRootActions(action, body, req, res, deps) {
|
|
|
14529
15274
|
return false;
|
|
14530
15275
|
}
|
|
14531
15276
|
}
|
|
14532
|
-
async function refreshLiveScope(deps, key, state) {
|
|
14533
|
-
const persisted = findRootInState(state, key);
|
|
14534
|
-
if (persisted === void 0) return;
|
|
14535
|
-
const raw = await Promise.resolve(deps.listRoots());
|
|
14536
|
-
const liveEntry = raw.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14537
|
-
if (liveEntry === void 0) return;
|
|
14538
|
-
if (liveScopeIsCurrent(liveEntry, persisted)) return;
|
|
14539
|
-
const liveKey = liveKeyOfRecord(liveEntry, persisted);
|
|
14540
|
-
if (liveKey === void 0) return;
|
|
14541
|
-
await deps.unregisterRoot(liveKey);
|
|
14542
|
-
await deps.registerRoot(persisted);
|
|
14543
|
-
}
|
|
14544
|
-
function liveScopeIsCurrent(raw, entry) {
|
|
14545
|
-
const outer = asRecord2(raw) ?? {};
|
|
14546
|
-
const source = asRecord2(outer["root"]) ?? outer;
|
|
14547
|
-
if (source["shared"] === true) return true;
|
|
14548
|
-
return sameStrings(source["workspaces"], entry.workspaces) && sameStrings(source["sessions"], entry.sessions);
|
|
14549
|
-
}
|
|
14550
|
-
function sameStrings(left, right) {
|
|
14551
|
-
const asSet = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").sort() : [];
|
|
14552
|
-
const a = asSet(left);
|
|
14553
|
-
const b = asSet(right);
|
|
14554
|
-
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
14555
|
-
}
|
|
14556
15277
|
function rootAddedBySession(state, key, sessionId) {
|
|
14557
15278
|
const entry = findRootInState(state, key);
|
|
14558
15279
|
return (entry?.sessions ?? []).includes(sessionId);
|
|
@@ -14607,129 +15328,6 @@ function optionalWorkspaceName(value) {
|
|
|
14607
15328
|
// src/host/app/roots/roots-api-connections.ts
|
|
14608
15329
|
init_errors();
|
|
14609
15330
|
import { homedir as homedir3 } from "node:os";
|
|
14610
|
-
|
|
14611
|
-
// src/host/domain/roots/ssh-config.ts
|
|
14612
|
-
function stripComment(line) {
|
|
14613
|
-
let inQuotes = false;
|
|
14614
|
-
for (let index = 0; index < line.length; index++) {
|
|
14615
|
-
const char = line[index];
|
|
14616
|
-
if (char === '"') inQuotes = !inQuotes;
|
|
14617
|
-
else if (char === "#" && !inQuotes) return line.slice(0, index);
|
|
14618
|
-
}
|
|
14619
|
-
return line;
|
|
14620
|
-
}
|
|
14621
|
-
function unquote(value) {
|
|
14622
|
-
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
14623
|
-
return value;
|
|
14624
|
-
}
|
|
14625
|
-
function splitKeyValue(line) {
|
|
14626
|
-
const match = /^([A-Za-z][A-Za-z0-9]*)\s*(?:=|\s)\s*(.*)$/.exec(line);
|
|
14627
|
-
if (match === null) return void 0;
|
|
14628
|
-
const key = match[1];
|
|
14629
|
-
const value = match[2];
|
|
14630
|
-
if (key === void 0 || value === void 0) return void 0;
|
|
14631
|
-
return [key.toLowerCase(), unquote(value.trim())];
|
|
14632
|
-
}
|
|
14633
|
-
function parseSshConfig(text8) {
|
|
14634
|
-
const entries = [];
|
|
14635
|
-
let current;
|
|
14636
|
-
const lines = text8.split(/\r?\n/);
|
|
14637
|
-
for (let index = 0; index < lines.length; index++) {
|
|
14638
|
-
const raw = lines[index] ?? "";
|
|
14639
|
-
const line = stripComment(raw).trim();
|
|
14640
|
-
if (line === "") continue;
|
|
14641
|
-
const pair = splitKeyValue(line);
|
|
14642
|
-
if (pair === void 0) continue;
|
|
14643
|
-
const [key, value] = pair;
|
|
14644
|
-
if (key === "host") {
|
|
14645
|
-
const patterns = value.split(/\s+/).filter((part) => part !== "");
|
|
14646
|
-
if (patterns.length === 0) continue;
|
|
14647
|
-
current = { patterns, identityFiles: [], includes: [], line: index + 1 };
|
|
14648
|
-
entries.push(current);
|
|
14649
|
-
continue;
|
|
14650
|
-
}
|
|
14651
|
-
if (current === void 0) continue;
|
|
14652
|
-
switch (key) {
|
|
14653
|
-
case "hostname":
|
|
14654
|
-
current.hostName ??= value;
|
|
14655
|
-
break;
|
|
14656
|
-
case "user":
|
|
14657
|
-
current.user ??= value;
|
|
14658
|
-
break;
|
|
14659
|
-
case "port": {
|
|
14660
|
-
if (current.port !== void 0) break;
|
|
14661
|
-
const port = Number.parseInt(value, 10);
|
|
14662
|
-
if (Number.isInteger(port) && port > 0 && port <= 65535) current.port = port;
|
|
14663
|
-
break;
|
|
14664
|
-
}
|
|
14665
|
-
case "identityfile":
|
|
14666
|
-
current.identityFiles.push(value);
|
|
14667
|
-
break;
|
|
14668
|
-
case "proxyjump":
|
|
14669
|
-
current.proxyJump ??= value.toLowerCase() === "none" ? "" : value;
|
|
14670
|
-
break;
|
|
14671
|
-
case "include":
|
|
14672
|
-
current.includes.push(value);
|
|
14673
|
-
break;
|
|
14674
|
-
default:
|
|
14675
|
-
break;
|
|
14676
|
-
}
|
|
14677
|
-
}
|
|
14678
|
-
return entries.map((entry) => ({
|
|
14679
|
-
patterns: entry.patterns,
|
|
14680
|
-
...entry.hostName === void 0 ? {} : { hostName: entry.hostName },
|
|
14681
|
-
...entry.user === void 0 ? {} : { user: entry.user },
|
|
14682
|
-
...entry.port === void 0 ? {} : { port: entry.port },
|
|
14683
|
-
identityFiles: entry.identityFiles,
|
|
14684
|
-
...entry.proxyJump === void 0 || entry.proxyJump === "" ? {} : { proxyJump: entry.proxyJump },
|
|
14685
|
-
includes: entry.includes,
|
|
14686
|
-
line: entry.line
|
|
14687
|
-
}));
|
|
14688
|
-
}
|
|
14689
|
-
function isConcretePattern(pattern) {
|
|
14690
|
-
return !/[*?!]/.test(pattern);
|
|
14691
|
-
}
|
|
14692
|
-
function expandHome2(path, home) {
|
|
14693
|
-
if (path === "~") return home;
|
|
14694
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return `${home.replace(/[\\/]+$/, "")}${path.slice(1)}`;
|
|
14695
|
-
return path;
|
|
14696
|
-
}
|
|
14697
|
-
function importableEntries(entries, options = {}) {
|
|
14698
|
-
const home = options.home ?? "";
|
|
14699
|
-
const excluded = new Set(options.excluded ?? []);
|
|
14700
|
-
const seen = /* @__PURE__ */ new Set();
|
|
14701
|
-
const drafts = [];
|
|
14702
|
-
const skipped = [];
|
|
14703
|
-
for (const entry of entries) {
|
|
14704
|
-
for (const pattern of entry.patterns) {
|
|
14705
|
-
if (!isConcretePattern(pattern)) {
|
|
14706
|
-
skipped.push({ pattern, reason: "wildcard", line: entry.line });
|
|
14707
|
-
continue;
|
|
14708
|
-
}
|
|
14709
|
-
if (seen.has(pattern)) {
|
|
14710
|
-
skipped.push({ pattern, reason: "duplicate", line: entry.line });
|
|
14711
|
-
continue;
|
|
14712
|
-
}
|
|
14713
|
-
if (excluded.has(pattern)) {
|
|
14714
|
-
skipped.push({ pattern, reason: "already-imported", line: entry.line });
|
|
14715
|
-
continue;
|
|
14716
|
-
}
|
|
14717
|
-
seen.add(pattern);
|
|
14718
|
-
const identityFile = entry.identityFiles[0];
|
|
14719
|
-
drafts.push({
|
|
14720
|
-
alias: pattern,
|
|
14721
|
-
hostName: entry.hostName ?? pattern,
|
|
14722
|
-
...entry.user === void 0 ? {} : { user: entry.user },
|
|
14723
|
-
...entry.port === void 0 ? {} : { port: entry.port },
|
|
14724
|
-
...identityFile === void 0 ? {} : { identityFile: home === "" ? identityFile : expandHome2(identityFile, home) },
|
|
14725
|
-
...entry.proxyJump === void 0 ? {} : { proxyJump: entry.proxyJump }
|
|
14726
|
-
});
|
|
14727
|
-
}
|
|
14728
|
-
}
|
|
14729
|
-
return { drafts, skipped };
|
|
14730
|
-
}
|
|
14731
|
-
|
|
14732
|
-
// src/host/app/roots/roots-api-connections.ts
|
|
14733
15331
|
async function handleConnectionActions(action, body, req, res, deps) {
|
|
14734
15332
|
switch (action) {
|
|
14735
15333
|
case "list-ssh-config": {
|
|
@@ -14748,14 +15346,12 @@ async function handleConnectionActions(action, body, req, res, deps) {
|
|
|
14748
15346
|
}
|
|
14749
15347
|
if (wanted.length === 0) throw new SshDevError("CONNECTION_INVALID", "\u6CA1\u6709\u9009\u62E9\u8981\u5BFC\u5165\u7684\u522B\u540D", {});
|
|
14750
15348
|
const before = await currentUserState(deps);
|
|
14751
|
-
const
|
|
14752
|
-
if (
|
|
15349
|
+
const plan = await planSshImport(deps, before.connections.map((entry) => entry.id));
|
|
15350
|
+
if (plan === void 0) {
|
|
14753
15351
|
throw new SshDevError("CONNECTION_INVALID", "\u8FD9\u4E2A\u5BBF\u4E3B\u7EC4\u5408\u6CA1\u6709\u63D0\u4F9B ~/.ssh/config \u7684\u8BFB\u53D6\u80FD\u529B", {});
|
|
14754
15352
|
}
|
|
14755
|
-
const byAlias = new Map(
|
|
14756
|
-
const
|
|
14757
|
-
const drafts = importableEntries(parseSshConfig(text8 ?? ""), { home: homedir3() }).drafts;
|
|
14758
|
-
const draftByAlias = new Map(drafts.map((draft) => [draft.alias, draft]));
|
|
15353
|
+
const byAlias = new Map(plan.candidates.map((candidate) => [candidate.alias, candidate]));
|
|
15354
|
+
const draftByAlias = new Map(plan.drafts.map((draft) => [draft.alias, draft]));
|
|
14759
15355
|
const accepted = [];
|
|
14760
15356
|
const refused = [];
|
|
14761
15357
|
const seenIds = new Set(before.connections.map((entry) => entry.id));
|
|
@@ -14773,7 +15369,10 @@ async function handleConnectionActions(action, body, req, res, deps) {
|
|
|
14773
15369
|
seenIds.add(candidate.suggestedId);
|
|
14774
15370
|
accepted.push(connectionRecordFromCandidate(draft, candidate.suggestedId));
|
|
14775
15371
|
}
|
|
14776
|
-
|
|
15372
|
+
if (accepted.length > 0) {
|
|
15373
|
+
if (deps.addConnections !== void 0) await deps.addConnections(accepted);
|
|
15374
|
+
else for (const record2 of accepted) await deps.addConnection(record2);
|
|
15375
|
+
}
|
|
14777
15376
|
const state = await reconcile(deps, (current) => {
|
|
14778
15377
|
let next = current;
|
|
14779
15378
|
for (const record2 of accepted) {
|
|
@@ -15015,11 +15614,11 @@ function publicProbeResult(raw) {
|
|
|
15015
15614
|
ms
|
|
15016
15615
|
};
|
|
15017
15616
|
}
|
|
15018
|
-
async function
|
|
15617
|
+
async function planSshImport(deps, excluded) {
|
|
15019
15618
|
const read = deps.readSshConfig;
|
|
15020
15619
|
if (read === void 0) return void 0;
|
|
15021
15620
|
const text8 = await read();
|
|
15022
|
-
if (text8 === void 0 || text8.trim() === "") return { candidates: [], skipped: [] };
|
|
15621
|
+
if (text8 === void 0 || text8.trim() === "") return { candidates: [], skipped: [], drafts: [] };
|
|
15023
15622
|
const result = importableEntries(parseSshConfig(text8), { home: homedir3(), excluded });
|
|
15024
15623
|
const taken = new Set(excluded);
|
|
15025
15624
|
const candidates = [];
|
|
@@ -15045,7 +15644,12 @@ async function sshConfigCandidates(deps, excluded) {
|
|
|
15045
15644
|
suggestedId
|
|
15046
15645
|
});
|
|
15047
15646
|
}
|
|
15048
|
-
return { candidates, skipped };
|
|
15647
|
+
return { candidates, skipped, drafts: result.drafts };
|
|
15648
|
+
}
|
|
15649
|
+
async function sshConfigCandidates(deps, excluded) {
|
|
15650
|
+
const plan = await planSshImport(deps, excluded);
|
|
15651
|
+
if (plan === void 0) return void 0;
|
|
15652
|
+
return { candidates: plan.candidates, skipped: plan.skipped };
|
|
15049
15653
|
}
|
|
15050
15654
|
function connectionRecordFromCandidate(draft, id) {
|
|
15051
15655
|
const keyPath = draft.identityFile === void 0 || draft.identityFile === "" ? void 0 : draft.identityFile;
|
|
@@ -15334,16 +15938,40 @@ async function serveSnapshot(req, res, deps) {
|
|
|
15334
15938
|
Promise.resolve(deps.listConnections()),
|
|
15335
15939
|
Promise.resolve(deps.loadUserState())
|
|
15336
15940
|
]);
|
|
15941
|
+
const query = queryOf(req.url);
|
|
15942
|
+
const requestedSession = (query.get("session") ?? "").trim();
|
|
15943
|
+
const requestedWorkspace = (query.get("workspace") ?? "").trim();
|
|
15944
|
+
const ancestorsOf = deps.resolveSessionAncestors;
|
|
15945
|
+
const canFilter = requestedWorkspace !== "" || ancestorsOf !== void 0;
|
|
15946
|
+
const filtered = !canFilter || requestedSession === "" && requestedWorkspace === "" ? void 0 : {
|
|
15947
|
+
ids: new Set(
|
|
15948
|
+
requestedSession === "" ? [] : [requestedSession, ...ancestorsOf?.(requestedSession) ?? []]
|
|
15949
|
+
),
|
|
15950
|
+
workspace: requestedWorkspace
|
|
15951
|
+
};
|
|
15952
|
+
const roots = filtered === void 0 ? liveRoots.map(publicRoot) : liveRoots.filter((raw) => visibleToSession(liveRootScopes(raw), filtered.ids, filtered.workspace)).map(publicRoot);
|
|
15337
15953
|
send(req, res, 200, {
|
|
15338
15954
|
ok: true,
|
|
15955
|
+
// 两个标记分开写:客户端只认 `scopedForSession`,而只带 workspace 的调用方也能看出"这份列表被过滤过"。
|
|
15956
|
+
...filtered === void 0 ? {} : requestedSession === "" ? { scopedForWorkspace: requestedWorkspace } : { scopedForSession: requestedSession },
|
|
15339
15957
|
// The plugin's **anchor root**, not the roots' individual anchor paths: see `RootsApiDeps.anchorRootDir`.
|
|
15340
15958
|
...deps.anchorRootDir === void 0 ? {} : { anchorRoot: deps.anchorRootDir },
|
|
15341
|
-
roots
|
|
15959
|
+
roots,
|
|
15342
15960
|
connections: liveConnections.map(publicConnection),
|
|
15343
15961
|
userRoots: publicUserState(loaded.state),
|
|
15344
15962
|
warnings: [...loaded.warnings]
|
|
15345
15963
|
});
|
|
15346
15964
|
}
|
|
15965
|
+
function queryOf(url) {
|
|
15966
|
+
const index = url === void 0 ? -1 : url.indexOf("?");
|
|
15967
|
+
return new URLSearchParams(index === -1 || url === void 0 ? "" : url.slice(index + 1));
|
|
15968
|
+
}
|
|
15969
|
+
function visibleToSession(row, sessionIds, workspace) {
|
|
15970
|
+
const sessions = row.sessions ?? [];
|
|
15971
|
+
if (sessions.length > 0) return sessions.some((id) => sessionIds.has(id));
|
|
15972
|
+
if (workspace === "") return false;
|
|
15973
|
+
return (row.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, workspace));
|
|
15974
|
+
}
|
|
15347
15975
|
async function serveAction(req, res, deps) {
|
|
15348
15976
|
let text8;
|
|
15349
15977
|
try {
|
|
@@ -15399,7 +16027,7 @@ async function serveAction(req, res, deps) {
|
|
|
15399
16027
|
|
|
15400
16028
|
// src/host/integration/session/session-bin-lock.ts
|
|
15401
16029
|
init_errors();
|
|
15402
|
-
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as
|
|
16030
|
+
import { mkdir as mkdir5, open as open3, readFile as readFile6, rm as rm3, stat as stat5 } from "node:fs/promises";
|
|
15403
16031
|
import { dirname as dirname5 } from "node:path";
|
|
15404
16032
|
var DEFAULT_STALE_LOCK_MS = 6e4;
|
|
15405
16033
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
@@ -15457,21 +16085,35 @@ function createSessionBinLock(options) {
|
|
|
15457
16085
|
const reclaimIfStale = async () => {
|
|
15458
16086
|
if (staleMs <= 0) return false;
|
|
15459
16087
|
let age;
|
|
16088
|
+
let malformed = false;
|
|
15460
16089
|
try {
|
|
15461
|
-
const info = await
|
|
16090
|
+
const info = await stat5(options.path);
|
|
16091
|
+
malformed = info.isDirectory();
|
|
15462
16092
|
age = now() - info.mtimeMs;
|
|
15463
16093
|
} catch {
|
|
15464
16094
|
return true;
|
|
15465
16095
|
}
|
|
15466
|
-
if (age < staleMs) return false;
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
16096
|
+
if (!malformed && age < staleMs) return false;
|
|
16097
|
+
if (!malformed) {
|
|
16098
|
+
try {
|
|
16099
|
+
const text8 = await readFile6(options.path, "utf8");
|
|
16100
|
+
const record2 = JSON.parse(text8);
|
|
16101
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: record2 });
|
|
16102
|
+
} catch {
|
|
16103
|
+
options.onReclaim?.({ path: options.path, ageMs: age, holder: {} });
|
|
16104
|
+
}
|
|
16105
|
+
} else {
|
|
15472
16106
|
options.onReclaim?.({ path: options.path, ageMs: age, holder: {} });
|
|
15473
16107
|
}
|
|
15474
|
-
|
|
16108
|
+
try {
|
|
16109
|
+
await rm3(options.path, { force: true, recursive: malformed });
|
|
16110
|
+
} catch (error) {
|
|
16111
|
+
throw new SshDevError("TRANSFER_CONFLICT", `\u4F1A\u8BDD\u56DE\u6536\u7AD9\u9501\u4F4D\u7F6E\u662F\u4E00\u4E2A\u76EE\u5F55\uFF0C\u4E14\u65E0\u6CD5\u5220\u9664\uFF1A${options.path}`, {
|
|
16112
|
+
path: options.path,
|
|
16113
|
+
reason: "malformed-lock",
|
|
16114
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
16115
|
+
});
|
|
16116
|
+
}
|
|
15475
16117
|
return true;
|
|
15476
16118
|
};
|
|
15477
16119
|
const withLock = async (_key, operation) => {
|
|
@@ -15537,6 +16179,12 @@ function createRootsWiring(deps) {
|
|
|
15537
16179
|
const transfers = deps.transfers;
|
|
15538
16180
|
const readCredentials = deps.readCredentials;
|
|
15539
16181
|
const testConnection = deps.testConnection;
|
|
16182
|
+
const sessionDirectories = createSessionDirectories({
|
|
16183
|
+
mutateUserState: (transition) => store2.mutate(transition),
|
|
16184
|
+
listRoots: () => deps.listRoots(),
|
|
16185
|
+
registerRoot: deps.registerRoot,
|
|
16186
|
+
unregisterRoot: deps.unregisterRoot
|
|
16187
|
+
});
|
|
15540
16188
|
const apiDeps = {
|
|
15541
16189
|
listRoots: () => deps.listRoots(),
|
|
15542
16190
|
listConnections: () => deps.listConnections(),
|
|
@@ -15553,6 +16201,39 @@ function createRootsWiring(deps) {
|
|
|
15553
16201
|
deps.applyConnection(entry, entry.id);
|
|
15554
16202
|
return next;
|
|
15555
16203
|
},
|
|
16204
|
+
addConnections: async (entries) => {
|
|
16205
|
+
if (entries.length === 0) return store2.load().then((loaded) => loaded.state);
|
|
16206
|
+
const applied = [];
|
|
16207
|
+
try {
|
|
16208
|
+
for (const entry of entries) {
|
|
16209
|
+
deps.applyConnection(entry, entry.id);
|
|
16210
|
+
applied.push(entry.id);
|
|
16211
|
+
}
|
|
16212
|
+
} catch (error) {
|
|
16213
|
+
for (const id of applied) {
|
|
16214
|
+
try {
|
|
16215
|
+
deps.removeConnection(id);
|
|
16216
|
+
} catch {
|
|
16217
|
+
}
|
|
16218
|
+
}
|
|
16219
|
+
throw error;
|
|
16220
|
+
}
|
|
16221
|
+
try {
|
|
16222
|
+
return await store2.mutate((state) => {
|
|
16223
|
+
let next = state;
|
|
16224
|
+
for (const entry of entries) next = stateHasConnection(next, entry.id) ? next : addConnectionToState(next, entry);
|
|
16225
|
+
return next;
|
|
16226
|
+
});
|
|
16227
|
+
} catch (error) {
|
|
16228
|
+
for (const id of applied) {
|
|
16229
|
+
try {
|
|
16230
|
+
deps.removeConnection(id);
|
|
16231
|
+
} catch {
|
|
16232
|
+
}
|
|
16233
|
+
}
|
|
16234
|
+
throw error;
|
|
16235
|
+
}
|
|
16236
|
+
},
|
|
15556
16237
|
applyConnection: async (entry, previousId) => {
|
|
15557
16238
|
deps.applyConnection(entry, previousId);
|
|
15558
16239
|
},
|
|
@@ -15560,6 +16241,7 @@ function createRootsWiring(deps) {
|
|
|
15560
16241
|
deps.removeConnection(id);
|
|
15561
16242
|
},
|
|
15562
16243
|
...deps.anchorRootDir === void 0 ? {} : { anchorRootDir: deps.anchorRootDir },
|
|
16244
|
+
...deps.resolveSessionAncestors === void 0 ? {} : { resolveSessionAncestors: deps.resolveSessionAncestors },
|
|
15563
16245
|
...listRemoteDirectory === void 0 ? {} : { listRemoteDirectory: (input) => listRemoteDirectory(input) },
|
|
15564
16246
|
...remoteHomeDirectory === void 0 ? {} : { remoteHomeDirectory },
|
|
15565
16247
|
...listLocalDirectory === void 0 ? {} : { listLocalDirectory: (input) => listLocalDirectory(input) },
|
|
@@ -15633,9 +16315,55 @@ function createRootsWiring(deps) {
|
|
|
15633
16315
|
}
|
|
15634
16316
|
return { roots, connections, warnings };
|
|
15635
16317
|
};
|
|
16318
|
+
const inheritAddedRoots = async (sourceSessionId, childSessionId) => {
|
|
16319
|
+
const source = sourceSessionId.trim();
|
|
16320
|
+
const child = childSessionId.trim();
|
|
16321
|
+
if (source === "" || child === "") return;
|
|
16322
|
+
const keys = [];
|
|
16323
|
+
await store2.mutate((current) => {
|
|
16324
|
+
let next = current;
|
|
16325
|
+
for (const entry of current.roots) {
|
|
16326
|
+
const sessions = entry.sessions ?? [];
|
|
16327
|
+
if (!sessions.includes(source) || sessions.includes(child)) continue;
|
|
16328
|
+
const key = userRootKey(entry);
|
|
16329
|
+
next = withSessions(next, key, [child]);
|
|
16330
|
+
keys.push(key);
|
|
16331
|
+
}
|
|
16332
|
+
return next;
|
|
16333
|
+
});
|
|
16334
|
+
if (keys.length > 0) {
|
|
16335
|
+
const { state } = await store2.load();
|
|
16336
|
+
for (const key of keys) await refreshLiveScope(apiDeps, key, state);
|
|
16337
|
+
}
|
|
16338
|
+
};
|
|
16339
|
+
const pruneSession = async (sessionId) => {
|
|
16340
|
+
const trimmed = sessionId.trim();
|
|
16341
|
+
if (trimmed === "") return { changed: [], removed: [] };
|
|
16342
|
+
const before = (await store2.load()).state;
|
|
16343
|
+
const touched = before.roots.filter((entry) => (entry.sessions ?? []).includes(trimmed));
|
|
16344
|
+
if (touched.length === 0) return { changed: [], removed: [] };
|
|
16345
|
+
const after = await store2.mutate((current) => pruneSessionFromRoots(current, trimmed));
|
|
16346
|
+
const afterKeys = new Set(after.roots.map(userRootKey));
|
|
16347
|
+
const removed = [];
|
|
16348
|
+
const changed = [];
|
|
16349
|
+
for (const entry of touched) {
|
|
16350
|
+
const key = userRootKey(entry);
|
|
16351
|
+
if (!afterKeys.has(key)) {
|
|
16352
|
+
removed.push(key);
|
|
16353
|
+
await deps.unregisterRoot(key);
|
|
16354
|
+
} else {
|
|
16355
|
+
changed.push(key);
|
|
16356
|
+
}
|
|
16357
|
+
}
|
|
16358
|
+
for (const key of changed) await refreshLiveScope(apiDeps, key, after);
|
|
16359
|
+
return { changed, removed };
|
|
16360
|
+
};
|
|
15636
16361
|
return {
|
|
15637
16362
|
api,
|
|
15638
16363
|
store: store2,
|
|
16364
|
+
sessionDirectories,
|
|
16365
|
+
inheritAddedRoots,
|
|
16366
|
+
pruneSession,
|
|
15639
16367
|
mergeUserRoots,
|
|
15640
16368
|
routeWarnings: () => [...routeWarnings],
|
|
15641
16369
|
routeFacts: () => ({ injectRan, routeRegistered, warnings: [...routeWarnings] }),
|
|
@@ -15778,13 +16506,34 @@ function createSessionApi(deps) {
|
|
|
15778
16506
|
return { handle, dispose: () => {
|
|
15779
16507
|
} };
|
|
15780
16508
|
}
|
|
16509
|
+
function abortOnDisconnect(res) {
|
|
16510
|
+
const controller = new AbortController();
|
|
16511
|
+
const closable = res;
|
|
16512
|
+
const onClose = () => {
|
|
16513
|
+
if (closable.writableEnded !== true) controller.abort();
|
|
16514
|
+
};
|
|
16515
|
+
if (typeof closable.on === "function") closable.on("close", onClose);
|
|
16516
|
+
return {
|
|
16517
|
+
signal: controller.signal,
|
|
16518
|
+
dispose: () => {
|
|
16519
|
+
if (typeof closable.off === "function") closable.off("close", onClose);
|
|
16520
|
+
}
|
|
16521
|
+
};
|
|
16522
|
+
}
|
|
15781
16523
|
async function serveSnapshot2(req, res, deps) {
|
|
15782
16524
|
const service = deps.service();
|
|
15783
16525
|
if (service === void 0) {
|
|
15784
16526
|
send(req, res, 200, { ok: true, available: false, sessions: [], bin: [], policy: deps.policy() });
|
|
15785
16527
|
return;
|
|
15786
16528
|
}
|
|
15787
|
-
const
|
|
16529
|
+
const connection = abortOnDisconnect(res);
|
|
16530
|
+
let sessions;
|
|
16531
|
+
let bin;
|
|
16532
|
+
try {
|
|
16533
|
+
[sessions, bin] = await Promise.all([service.inventory({ signal: connection.signal }), service.list()]);
|
|
16534
|
+
} finally {
|
|
16535
|
+
connection.dispose();
|
|
16536
|
+
}
|
|
15788
16537
|
send(req, res, 200, {
|
|
15789
16538
|
ok: true,
|
|
15790
16539
|
available: true,
|
|
@@ -15801,10 +16550,19 @@ async function serveAction2(req, res, deps) {
|
|
|
15801
16550
|
return;
|
|
15802
16551
|
}
|
|
15803
16552
|
const service = requireService(deps);
|
|
16553
|
+
const connection = abortOnDisconnect(res);
|
|
16554
|
+
try {
|
|
16555
|
+
return await serveActionWithSignal(action, body, req, res, deps, service, connection.signal);
|
|
16556
|
+
} finally {
|
|
16557
|
+
connection.dispose();
|
|
16558
|
+
}
|
|
16559
|
+
}
|
|
16560
|
+
async function serveActionWithSignal(action, body, req, res, deps, service, signal) {
|
|
16561
|
+
const options = { signal };
|
|
15804
16562
|
switch (action) {
|
|
15805
16563
|
case "preview": {
|
|
15806
16564
|
const ids = readIds(body);
|
|
15807
|
-
const decision = await service.previewDelete(ids);
|
|
16565
|
+
const decision = await service.previewDelete(ids, options);
|
|
15808
16566
|
send(req, res, 200, { ok: true, delete: decision.delete, refusals: decision.refusals, bytes: decision.bytes });
|
|
15809
16567
|
return;
|
|
15810
16568
|
}
|
|
@@ -15819,7 +16577,7 @@ async function serveAction2(req, res, deps) {
|
|
|
15819
16577
|
});
|
|
15820
16578
|
return;
|
|
15821
16579
|
}
|
|
15822
|
-
const result = await service.deleteSession(ids, { mode });
|
|
16580
|
+
const result = await service.deleteSession(ids, { mode, ...options });
|
|
15823
16581
|
send(req, res, 200, {
|
|
15824
16582
|
ok: true,
|
|
15825
16583
|
mode: result.mode,
|
|
@@ -15847,7 +16605,7 @@ async function serveAction2(req, res, deps) {
|
|
|
15847
16605
|
});
|
|
15848
16606
|
return;
|
|
15849
16607
|
}
|
|
15850
|
-
const result = await service.deleteWorkspaceSessions(cwd, { mode });
|
|
16608
|
+
const result = await service.deleteWorkspaceSessions(cwd, { mode, ...options });
|
|
15851
16609
|
send(req, res, 200, {
|
|
15852
16610
|
ok: true,
|
|
15853
16611
|
mode: result.mode,
|
|
@@ -15867,7 +16625,7 @@ async function serveAction2(req, res, deps) {
|
|
|
15867
16625
|
send(req, res, 400, { ok: false, error: "entryId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" });
|
|
15868
16626
|
return;
|
|
15869
16627
|
}
|
|
15870
|
-
const result = await service.restore(entryId);
|
|
16628
|
+
const result = await service.restore(entryId, options);
|
|
15871
16629
|
send(req, res, 200, { ok: true, restoredPath: result.restoredPath, movedBy: result.movedBy });
|
|
15872
16630
|
return;
|
|
15873
16631
|
}
|
|
@@ -15881,14 +16639,27 @@ async function serveAction2(req, res, deps) {
|
|
|
15881
16639
|
send(req, res, 400, { ok: false, error: "purge \u9700\u8981 confirm: true\uFF08\u4E0D\u53EF\u6062\u590D\uFF09", reason: "confirmation-required" });
|
|
15882
16640
|
return;
|
|
15883
16641
|
}
|
|
15884
|
-
const result = await service.purge(entryId);
|
|
15885
|
-
send(req, res, 200, {
|
|
16642
|
+
const result = await service.purge(entryId, options);
|
|
16643
|
+
send(req, res, 200, {
|
|
16644
|
+
...result.indexStale === true ? { ok: false, error: "\u56DE\u6536\u7AD9\u7D22\u5F15\u66F4\u65B0\u5931\u8D25\uFF1A\u6761\u76EE\u5DF2\u5220\u9664\uFF0C\u4F46\u7D22\u5F15\u4ECD\u5217\u7740\u5B83\uFF08\u4E0B\u6B21\u8FD8\u539F\u4F1A\u5931\u8D25\uFF09\u3002\u91CD\u542F\u540E\u91CD\u8BD5 sweep\u3002", reason: "index-stale" } : { ok: true },
|
|
16645
|
+
purged: result.purged,
|
|
16646
|
+
indexStale: result.indexStale
|
|
16647
|
+
});
|
|
15886
16648
|
return;
|
|
15887
16649
|
}
|
|
15888
16650
|
case "sweep": {
|
|
15889
|
-
|
|
16651
|
+
if (body["confirm"] !== true) {
|
|
16652
|
+
send(req, res, 400, {
|
|
16653
|
+
ok: false,
|
|
16654
|
+
error: "sweep \u9700\u8981 confirm: true\uFF08\u4E0D\u53EF\u6062\u590D\uFF09",
|
|
16655
|
+
reason: "confirmation-required"
|
|
16656
|
+
});
|
|
16657
|
+
return;
|
|
16658
|
+
}
|
|
16659
|
+
const result = await service.sweep(deps.policy(), options);
|
|
16660
|
+
const stale = result.purged.filter((item) => item.indexStale === true).length;
|
|
15890
16661
|
send(req, res, 200, {
|
|
15891
|
-
ok: true,
|
|
16662
|
+
...stale > 0 ? { ok: false, error: `\u56DE\u6536\u7AD9\u7D22\u5F15\u66F4\u65B0\u5931\u8D25\uFF1A${stale} \u4E2A\u6761\u76EE\u5DF2\u5220\u9664\u4F46\u7D22\u5F15\u4ECD\u5217\u7740\u5B83\u4EEC\uFF08\u4E0B\u6B21\u8FD8\u539F\u4F1A\u5931\u8D25\uFF09\u3002`, reason: "index-stale" } : { ok: true },
|
|
15892
16663
|
purged: result.purged.map((item) => ({ entryId: String(item.id), sessionId: item.sessionId, indexStale: item.indexStale })),
|
|
15893
16664
|
failed: result.failed.map((item) => ({ entryId: String(item.id), reason: item.reason })),
|
|
15894
16665
|
remainingEntries: result.remainingEntries,
|
|
@@ -15963,6 +16734,56 @@ function createSessionRouteWiring(deps) {
|
|
|
15963
16734
|
};
|
|
15964
16735
|
}
|
|
15965
16736
|
|
|
16737
|
+
// src/host/integration/roots/fork-inheritance.ts
|
|
16738
|
+
var PATCHED = /* @__PURE__ */ Symbol.for("dsh-over-ssh/fork-inheritance");
|
|
16739
|
+
function sourceIdOf(source) {
|
|
16740
|
+
if (typeof source === "string") return source === "" ? void 0 : source;
|
|
16741
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
16742
|
+
const candidate = source;
|
|
16743
|
+
const id = candidate.header?.id ?? candidate.id;
|
|
16744
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
16745
|
+
}
|
|
16746
|
+
function installForkInheritance(ctx, deps) {
|
|
16747
|
+
const service = ctx.get("sessions");
|
|
16748
|
+
const original = service?.fork;
|
|
16749
|
+
if (typeof service !== "object" || service === null || typeof original !== "function") {
|
|
16750
|
+
return { installed: false, dispose: () => void 0 };
|
|
16751
|
+
}
|
|
16752
|
+
if (original[PATCHED] === true) {
|
|
16753
|
+
return { installed: true, dispose: () => void 0 };
|
|
16754
|
+
}
|
|
16755
|
+
const invoke = original;
|
|
16756
|
+
const patched2 = function patchedFork(...args) {
|
|
16757
|
+
const child = invoke.apply(this, args);
|
|
16758
|
+
try {
|
|
16759
|
+
const sourceId = sourceIdOf(args[0]);
|
|
16760
|
+
const childId = sourceIdOf(child);
|
|
16761
|
+
if (sourceId !== void 0 && childId !== void 0) {
|
|
16762
|
+
markForkInheritancePending(childId, sourceId);
|
|
16763
|
+
void deps.inherit(sourceId, childId).catch((error) => {
|
|
16764
|
+
deps.warn?.(`fork \u7EE7\u627F\u6DFB\u52A0\u76EE\u5F55\u5931\u8D25\uFF08source=${sourceId} child=${childId}\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16765
|
+
}).finally(() => {
|
|
16766
|
+
clearForkInheritancePending(childId);
|
|
16767
|
+
});
|
|
16768
|
+
}
|
|
16769
|
+
} catch (error) {
|
|
16770
|
+
deps.warn?.(`fork \u7EE7\u627F\u6DFB\u52A0\u76EE\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16771
|
+
}
|
|
16772
|
+
return child;
|
|
16773
|
+
};
|
|
16774
|
+
patched2[PATCHED] = true;
|
|
16775
|
+
service.fork = patched2;
|
|
16776
|
+
return {
|
|
16777
|
+
installed: true,
|
|
16778
|
+
dispose: () => {
|
|
16779
|
+
if (service.fork === patched2) {
|
|
16780
|
+
service.fork = original;
|
|
16781
|
+
}
|
|
16782
|
+
clearAllForkInheritancePending();
|
|
16783
|
+
}
|
|
16784
|
+
};
|
|
16785
|
+
}
|
|
16786
|
+
|
|
15966
16787
|
// src/wiring/fs-panel-routes.ts
|
|
15967
16788
|
function readOnlySeams(env) {
|
|
15968
16789
|
const { ctx, profiles, registry, transports } = env;
|
|
@@ -16008,10 +16829,15 @@ function rootLifecycleSeams(env, hooks) {
|
|
|
16008
16829
|
const removed = registry.removeByLooseKey(key);
|
|
16009
16830
|
binRootResolver.forget(removed ?? key);
|
|
16010
16831
|
sessionRoots.invalidate();
|
|
16011
|
-
if (anchor !== void 0
|
|
16832
|
+
if (anchor !== void 0 && !anchorStillUsedBy(registry.records, anchor)) {
|
|
16833
|
+
void rmdir(anchor).catch(() => void 0);
|
|
16834
|
+
}
|
|
16012
16835
|
}
|
|
16013
16836
|
};
|
|
16014
16837
|
}
|
|
16838
|
+
function anchorStillUsedBy(records, anchor) {
|
|
16839
|
+
return records.some((record2) => record2.root.anchorPath === anchor);
|
|
16840
|
+
}
|
|
16015
16841
|
function anchorPathOfRecord(records, key) {
|
|
16016
16842
|
const target = key.toLowerCase();
|
|
16017
16843
|
for (const record2 of records) {
|
|
@@ -16030,18 +16856,18 @@ function connectionLifecycleSeams(env, hooks) {
|
|
|
16030
16856
|
// Keeping them across an edit answered for the previous host (audit finding: changing a connection's
|
|
16031
16857
|
// host left its remote roots connected to the old machine); keeping them across a rename answered under
|
|
16032
16858
|
// the new name for the old host.
|
|
16033
|
-
applyConnection: (entry, previousId) => {
|
|
16859
|
+
applyConnection: async (entry, previousId) => {
|
|
16034
16860
|
const profile = validateConnectionProfile(toProfileInput({ ...entry, transport: "agent" }));
|
|
16035
16861
|
profiles.set(profile.id, profile);
|
|
16036
16862
|
if (previousId !== profile.id) {
|
|
16037
16863
|
profiles.delete(previousId);
|
|
16038
|
-
hooks.forgetPerConnection(previousId);
|
|
16864
|
+
await hooks.forgetPerConnection(previousId);
|
|
16039
16865
|
indexes.delete(previousId);
|
|
16040
16866
|
retireBrowseBackend(browseBackends, previousId);
|
|
16041
16867
|
forgetBackendsOf(previousId);
|
|
16042
16868
|
void transports.release(previousId).catch(() => void 0);
|
|
16043
16869
|
}
|
|
16044
|
-
hooks.forgetPerConnection(profile.id);
|
|
16870
|
+
await hooks.forgetPerConnection(profile.id);
|
|
16045
16871
|
askpass.revive(profile.id);
|
|
16046
16872
|
indexes.delete(profile.id);
|
|
16047
16873
|
retireBrowseBackend(browseBackends, profile.id);
|
|
@@ -16175,6 +17001,8 @@ function registerPanelRoutes(env, hooks) {
|
|
|
16175
17001
|
stateDir: resolveStateDir(),
|
|
16176
17002
|
// Published in the GET snapshot so a client can recognise a session whose cwd is a remote workspace.
|
|
16177
17003
|
anchorRootDir: env.anchorRootDir,
|
|
17004
|
+
// fork 继承父会话添加的目录(ADR-0013):血缘只有宿主知道,所以 `GET /roots?session=` 由这里回答。
|
|
17005
|
+
resolveSessionAncestors: (sessionId) => readSessionAncestors(env.ctx, sessionId),
|
|
16178
17006
|
...readOnlySeams(env),
|
|
16179
17007
|
...rootLifecycleSeams(env, hooks),
|
|
16180
17008
|
...connectionLifecycleSeams(env, hooks),
|
|
@@ -16182,7 +17010,11 @@ function registerPanelRoutes(env, hooks) {
|
|
|
16182
17010
|
...remoteFileSeams(env),
|
|
16183
17011
|
webServerProvider: ctx
|
|
16184
17012
|
});
|
|
16185
|
-
|
|
17013
|
+
const forkInheritance = installForkInheritance(ctx, {
|
|
17014
|
+
inherit: (sourceSessionId, childSessionId) => rootsWiring.inheritAddedRoots(sourceSessionId, childSessionId),
|
|
17015
|
+
warn: (message) => ctx.logger?.warn?.(`over-ssh: ${message}`)
|
|
17016
|
+
});
|
|
17017
|
+
return { sessionWiring, rootsWiring, disposeForkInheritance: () => forkInheritance.dispose() };
|
|
16186
17018
|
}
|
|
16187
17019
|
|
|
16188
17020
|
// src/wiring/fs.ts
|
|
@@ -16213,11 +17045,19 @@ function provideHostServices(env) {
|
|
|
16213
17045
|
}
|
|
16214
17046
|
function provideAgentStatus(env) {
|
|
16215
17047
|
const { ctx, agentBridge, installations, askpass, remoteHome } = env;
|
|
16216
|
-
const
|
|
17048
|
+
const forgetPerConnectionAsync = async (connectionId) => {
|
|
16217
17049
|
agentBridge.forget(connectionId);
|
|
16218
17050
|
installations.forget(connectionId);
|
|
16219
|
-
void askpass.forget(connectionId);
|
|
16220
17051
|
remoteHome.forget(connectionId);
|
|
17052
|
+
try {
|
|
17053
|
+
await askpass.forget(connectionId);
|
|
17054
|
+
} catch {
|
|
17055
|
+
}
|
|
17056
|
+
};
|
|
17057
|
+
const forgetPerConnection = (connectionId) => {
|
|
17058
|
+
void forgetPerConnectionAsync(connectionId).catch((error) => {
|
|
17059
|
+
ctx.logger?.warn?.(`over-ssh: \u5FD8\u8BB0\u8FDE\u63A5 ${connectionId} \u7684\u4EE3\u7406\u5B89\u88C5\u8BB0\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17060
|
+
});
|
|
16221
17061
|
};
|
|
16222
17062
|
const disposeAgent = ctx.provide("sshDevAgent", {
|
|
16223
17063
|
outcome: (connectionId) => agentBridge.installationFor(connectionId),
|
|
@@ -16225,7 +17065,7 @@ function provideAgentStatus(env) {
|
|
|
16225
17065
|
forgetPerConnection(connectionId);
|
|
16226
17066
|
}
|
|
16227
17067
|
});
|
|
16228
|
-
return { forgetPerConnection, disposeAgent };
|
|
17068
|
+
return { forgetPerConnection, forgetPerConnectionAsync, disposeAgent };
|
|
16229
17069
|
}
|
|
16230
17070
|
function createBinHousekeeping(env) {
|
|
16231
17071
|
const { ctx, limits, binService } = env;
|
|
@@ -16250,7 +17090,9 @@ function registerConfiguredRoots(env) {
|
|
|
16250
17090
|
ctx.logger?.warn?.(`over-ssh: \u8FDC\u7AEF\u6839\u7684\u951A\u70B9\u76EE\u5F55\u65E0\u6CD5\u521B\u5EFA\uFF08${root.anchorPath ?? ""}\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
16251
17091
|
});
|
|
16252
17092
|
}
|
|
16253
|
-
void Promise.allSettled(binRootPreparation).then(runBinHousekeeping)
|
|
17093
|
+
void Promise.allSettled(binRootPreparation).then(runBinHousekeeping).catch((error) => {
|
|
17094
|
+
ctx.logger?.warn?.(`over-ssh: \u56DE\u6536\u7AD9\u6E05\u7406\u7531\u672C\u6B21\u542F\u52A8\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
17095
|
+
});
|
|
16254
17096
|
}
|
|
16255
17097
|
function scheduleActivationDiagnostics(env) {
|
|
16256
17098
|
const { ctx, rootsWiring, sessionWiring } = env;
|
|
@@ -16276,7 +17118,7 @@ function scheduleActivationDiagnostics(env) {
|
|
|
16276
17118
|
function watchSessionRemovals(env) {
|
|
16277
17119
|
const { ctx, rootsWiring, onSessionRemoved } = env;
|
|
16278
17120
|
onSessionRemoved((sessionId) => {
|
|
16279
|
-
void rootsWiring.
|
|
17121
|
+
void rootsWiring.pruneSession(sessionId).catch((error) => {
|
|
16280
17122
|
ctx.logger?.warn?.(
|
|
16281
17123
|
`dsh-over-ssh: \u6E05\u7406\u5DF2\u5220\u4F1A\u8BDD ${sessionId} \u7684\u6839\u8BB0\u5F55\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
16282
17124
|
);
|
|
@@ -16292,9 +17134,10 @@ function mergePersistedRoots(env) {
|
|
|
16292
17134
|
});
|
|
16293
17135
|
}
|
|
16294
17136
|
function createTeardown(env) {
|
|
16295
|
-
const { activationTimers, routes, hostServices, disposeAgent, tools } = env;
|
|
17137
|
+
const { activationTimers, routes, hostServices, disposeAgent, tools, disposeSessionDirectories } = env;
|
|
16296
17138
|
return async () => {
|
|
16297
17139
|
for (const timer of activationTimers) clearTimeout(timer);
|
|
17140
|
+
routes.disposeForkInheritance();
|
|
16298
17141
|
routes.rootsWiring.dispose();
|
|
16299
17142
|
routes.sessionWiring.dispose();
|
|
16300
17143
|
tools.disposeManagement();
|
|
@@ -16305,6 +17148,7 @@ function createTeardown(env) {
|
|
|
16305
17148
|
await tools.disposeLsp();
|
|
16306
17149
|
hostServices.disposeBin();
|
|
16307
17150
|
hostServices.disposeSessionTools();
|
|
17151
|
+
disposeSessionDirectories();
|
|
16308
17152
|
hostServices.disposeSessionRoots();
|
|
16309
17153
|
hostServices.disposeRange();
|
|
16310
17154
|
hostServices.disposeTransfer();
|
|
@@ -16335,7 +17179,7 @@ function wireFs(env) {
|
|
|
16335
17179
|
sessionRoots: env.sessionRoots,
|
|
16336
17180
|
readBytesRange: env.readBytesRange
|
|
16337
17181
|
});
|
|
16338
|
-
const {
|
|
17182
|
+
const { forgetPerConnectionAsync, disposeAgent } = provideAgentStatus({
|
|
16339
17183
|
ctx,
|
|
16340
17184
|
agentBridge: env.agentBridge,
|
|
16341
17185
|
installations: env.installations,
|
|
@@ -16345,14 +17189,19 @@ function wireFs(env) {
|
|
|
16345
17189
|
const runBinHousekeeping = createBinHousekeeping({ ctx, limits, binService: env.binService });
|
|
16346
17190
|
registerConfiguredRoots({ ctx, config, anchorRootDir, registry, prepareBinRoots, runBinHousekeeping });
|
|
16347
17191
|
const routes = registerPanelRoutes(env, {
|
|
16348
|
-
forgetPerConnection,
|
|
17192
|
+
forgetPerConnection: forgetPerConnectionAsync,
|
|
16349
17193
|
runBinHousekeeping,
|
|
16350
17194
|
warn: (message) => ctx.logger?.warn?.(`over-ssh: ${message}`)
|
|
16351
17195
|
});
|
|
17196
|
+
const disposeSessionDirectories = registerSessionDirectoryTools(ctx, {
|
|
17197
|
+
directories: routes.rootsWiring.sessionDirectories,
|
|
17198
|
+
sessionId: () => currentSessionId(),
|
|
17199
|
+
workspacePathOf: (sessionId) => readSessionCwd(ctx, sessionId)
|
|
17200
|
+
});
|
|
16352
17201
|
const activationTimers = scheduleActivationDiagnostics({ ctx, ...routes });
|
|
16353
17202
|
watchSessionRemovals({ ctx, rootsWiring: routes.rootsWiring, onSessionRemoved: env.onSessionRemoved });
|
|
16354
17203
|
mergePersistedRoots({ ctx, rootsWiring: routes.rootsWiring });
|
|
16355
|
-
return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env });
|
|
17204
|
+
return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env, disposeSessionDirectories });
|
|
16356
17205
|
});
|
|
16357
17206
|
}
|
|
16358
17207
|
|
|
@@ -16366,7 +17215,7 @@ import { dirname as dirname7, join as join16 } from "node:path";
|
|
|
16366
17215
|
// src/host/integration/transfer/transfer-adapters.ts
|
|
16367
17216
|
init_errors();
|
|
16368
17217
|
import { createHash as createHash3 } from "node:crypto";
|
|
16369
|
-
import { mkdir as mkdir8, open as open4, readFile as readFile8, readdir as readdir5, rename as rename4, rm as rm4, stat as
|
|
17218
|
+
import { mkdir as mkdir8, open as open4, readFile as readFile8, readdir as readdir5, rename as rename4, rm as rm4, stat as stat6, writeFile as writeFile5 } from "node:fs/promises";
|
|
16370
17219
|
import { dirname as dirname6 } from "node:path";
|
|
16371
17220
|
function requireRemote(deps, path) {
|
|
16372
17221
|
const target = deps.routed.resolve(path);
|
|
@@ -16430,7 +17279,7 @@ function createTransferDeps(deps) {
|
|
|
16430
17279
|
},
|
|
16431
17280
|
async statLocal(path) {
|
|
16432
17281
|
try {
|
|
16433
|
-
const info = await
|
|
17282
|
+
const info = await stat6(path);
|
|
16434
17283
|
return toBackendStat(info, localVersion(info));
|
|
16435
17284
|
} catch {
|
|
16436
17285
|
return toBackendStat(void 0, "0:0");
|
|
@@ -16645,6 +17494,7 @@ function trimSlashes(value) {
|
|
|
16645
17494
|
// src/host/integration/roots/bin-root-resolver.ts
|
|
16646
17495
|
function createBinRootResolver(deps) {
|
|
16647
17496
|
const choices = /* @__PURE__ */ new Map();
|
|
17497
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
16648
17498
|
const fallback = (root, note) => ({
|
|
16649
17499
|
binRoot: binRootInsideRoot(root, deps.binDir),
|
|
16650
17500
|
placement: "inside",
|
|
@@ -16665,6 +17515,17 @@ function createBinRootResolver(deps) {
|
|
|
16665
17515
|
const key = rootKey(root);
|
|
16666
17516
|
const existing = choices.get(key);
|
|
16667
17517
|
if (existing !== void 0) return existing;
|
|
17518
|
+
const running = inFlight.get(key);
|
|
17519
|
+
if (running !== void 0) return running;
|
|
17520
|
+
const attempt = resolveBinRoot(root);
|
|
17521
|
+
inFlight.set(key, attempt);
|
|
17522
|
+
try {
|
|
17523
|
+
return await attempt;
|
|
17524
|
+
} finally {
|
|
17525
|
+
if (inFlight.get(key) === attempt) inFlight.delete(key);
|
|
17526
|
+
}
|
|
17527
|
+
};
|
|
17528
|
+
const resolveBinRoot = async (root) => {
|
|
16668
17529
|
const beside = binRootBesideRoot(root, deps.binDir);
|
|
16669
17530
|
const parent = binParentDirectory(root);
|
|
16670
17531
|
if (beside === void 0 || parent === void 0) {
|
|
@@ -17169,7 +18030,7 @@ function createSessionRootIndex(deps) {
|
|
|
17169
18030
|
const now = deps.now ?? (() => Date.now());
|
|
17170
18031
|
let cachedAt;
|
|
17171
18032
|
let cached2 = [];
|
|
17172
|
-
let
|
|
18033
|
+
let pending2;
|
|
17173
18034
|
let generation = 0;
|
|
17174
18035
|
const readEntries = async () => {
|
|
17175
18036
|
try {
|
|
@@ -17183,14 +18044,14 @@ function createSessionRootIndex(deps) {
|
|
|
17183
18044
|
const entriesNow = () => {
|
|
17184
18045
|
const at = cachedAt;
|
|
17185
18046
|
const fresh = at !== void 0 && now() - at < deps.ttlMs;
|
|
17186
|
-
if (!fresh &&
|
|
18047
|
+
if (!fresh && pending2 === void 0) {
|
|
17187
18048
|
const startedAt = generation;
|
|
17188
|
-
|
|
18049
|
+
pending2 = readEntries().then((entries) => {
|
|
17189
18050
|
if (startedAt === generation) {
|
|
17190
18051
|
cached2 = entries;
|
|
17191
18052
|
cachedAt = now();
|
|
17192
18053
|
}
|
|
17193
|
-
|
|
18054
|
+
pending2 = void 0;
|
|
17194
18055
|
return entries;
|
|
17195
18056
|
});
|
|
17196
18057
|
}
|
|
@@ -17205,7 +18066,7 @@ function createSessionRootIndex(deps) {
|
|
|
17205
18066
|
generation += 1;
|
|
17206
18067
|
cachedAt = void 0;
|
|
17207
18068
|
cached2 = [];
|
|
17208
|
-
|
|
18069
|
+
pending2 = void 0;
|
|
17209
18070
|
}
|
|
17210
18071
|
};
|
|
17211
18072
|
}
|
|
@@ -17238,7 +18099,7 @@ function execWriteRootsFor(input) {
|
|
|
17238
18099
|
}
|
|
17239
18100
|
|
|
17240
18101
|
// src/host/integration/tools/remote-tools.ts
|
|
17241
|
-
import { defineTool as
|
|
18102
|
+
import { defineTool as defineTool3 } from "@deepseek-ai/dsh-tools";
|
|
17242
18103
|
|
|
17243
18104
|
// src/host/integration/tools/session-target.ts
|
|
17244
18105
|
var NO_SESSION_CWD = "\u65E0\u6CD5\u786E\u5B9A\u8C03\u7528\u65B9\u7684\u8FDC\u7AEF\u6839\uFF1A\u8BE5\u5DE5\u5177\u9700\u8981 agent \u7684\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55";
|
|
@@ -17264,8 +18125,12 @@ function resolveSessionTarget(exec, seams) {
|
|
|
17264
18125
|
function readConnectionIds(record2) {
|
|
17265
18126
|
const raw = record2["connectionIds"];
|
|
17266
18127
|
if (!Array.isArray(raw)) return void 0;
|
|
18128
|
+
if (raw.length > MAX_EXEC_BATCH_CONNECTIONS) {
|
|
18129
|
+
throw new Error(`connectionIds \u4E00\u6B21\u6700\u591A ${MAX_EXEC_BATCH_CONNECTIONS} \u4E2A\uFF08\u6536\u5230 ${raw.length} \u4E2A\uFF09\u3002\u8BF7\u5206\u6279\u6267\u884C\u3002`);
|
|
18130
|
+
}
|
|
17267
18131
|
const ids = [...new Set(raw.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()))];
|
|
17268
|
-
|
|
18132
|
+
if (ids.length === 0) return void 0;
|
|
18133
|
+
return ids;
|
|
17269
18134
|
}
|
|
17270
18135
|
function text2(value) {
|
|
17271
18136
|
return [{ type: "text", text: value }];
|
|
@@ -17283,7 +18148,7 @@ var READONLY_CONCURRENCY_NOTE = " Read-only: calls to this tool may overlap with
|
|
|
17283
18148
|
var EXEC_SERIAL_NOTE = " This tool is deliberately NOT concurrency-safe: remote commands run one at a time per session, so `Promise.all` over several remote_exec calls produces no overlap. That protects a shared build server from concurrent `make` runs; use one command (`a && b`) or a background job instead of relying on parallelism.";
|
|
17284
18149
|
function remoteToolDefinitions(backend) {
|
|
17285
18150
|
return [
|
|
17286
|
-
|
|
18151
|
+
defineTool3({
|
|
17287
18152
|
name: "remote_grep",
|
|
17288
18153
|
description: "Search file contents inside the remote workspace this session is working in. Returns matching lines with their file and 1-based line number. Case-insensitive and ignore-aware by default; `caseSensitive` and `noIgnore` change that. Bounded: at most 200 matches, and the result says so when the cap was reached. When the agent answers, `noIgnore` also stops `.gitignore` from hiding files; on the plain-`grep` fallback tier VCS ignores were never applied, so there the flag only controls the built-in noise directories. The result names the tier that answered in `strategy`." + READONLY_CONCURRENCY_NOTE,
|
|
17289
18154
|
isConcurrencySafe: () => true,
|
|
@@ -17356,7 +18221,7 @@ function remoteToolDefinitions(backend) {
|
|
|
17356
18221
|
};
|
|
17357
18222
|
}
|
|
17358
18223
|
}),
|
|
17359
|
-
|
|
18224
|
+
defineTool3({
|
|
17360
18225
|
name: "remote_glob",
|
|
17361
18226
|
description: "List files in the remote workspace this session is working in by **glob pattern**, matched the same way on every transport. A pattern without `/` matches the file name at any depth (`*.h` finds `app/include/header.h`); a pattern with `/` is anchored at the directory you pass in and `*` never crosses a `/`. `**` crosses zero or more segments, `?` is one character, `[abc]` and `[!abc]` are character classes, `{a,b}` alternates. Matching is case-sensitive. Paths are returned relative to the enumerated directory. An invalid pattern (an unclosed `[` or `{`) is rejected rather than answered with an empty list. Bounded: at most 500 paths, and the result says so when the cap was reached." + READONLY_CONCURRENCY_NOTE,
|
|
17362
18227
|
isConcurrencySafe: () => true,
|
|
@@ -17403,7 +18268,7 @@ function remoteToolDefinitions(backend) {
|
|
|
17403
18268
|
};
|
|
17404
18269
|
}
|
|
17405
18270
|
}),
|
|
17406
|
-
|
|
18271
|
+
defineTool3({
|
|
17407
18272
|
name: "remote_exec",
|
|
17408
18273
|
description: "Run one shell command on the remote host in the workspace this session is working in, and return its exit code, stdout and stderr. **A shell command is not confined by \u300C\u6DFB\u52A0\u76EE\u5F55\u300D**: it runs with the SSH user's own permissions, so it can read and write anything that user can, not only the directories added to this conversation. (A deployment that sets `limits.execWriteFence` puts a Landlock write fence around the workspace root and the directories this session added; the tool report says when that fence is active.) Output is bounded to 64 KiB per stream; when a stream is cut the result names that stream and, if the agent spilled it, the remote path holding the full text. Use this for remote builds, tests and version checks. Each call is a NEW shell: exported variables and `cd` do not carry over to the next call \u2014 pass `env` or put `source ... && cmd` in one command." + EXEC_SERIAL_NOTE,
|
|
17409
18274
|
parameters: {
|
|
@@ -17725,13 +18590,14 @@ function strictArgumentsGate(ctx) {
|
|
|
17725
18590
|
|
|
17726
18591
|
// src/host/integration/tools/remote-job-tools.ts
|
|
17727
18592
|
init_errors();
|
|
17728
|
-
import { defineTool as
|
|
18593
|
+
import { defineTool as defineTool4 } from "@deepseek-ai/dsh-tools";
|
|
17729
18594
|
var DEFAULT_JOB_WAIT_MS = 3e4;
|
|
17730
18595
|
var MAX_JOB_WAIT_MS = 6e5;
|
|
17731
18596
|
var DEFAULT_JOB_POLL_MS = 500;
|
|
17732
18597
|
var WAIT_SLICE_MS = 5e3;
|
|
17733
18598
|
var DEFAULT_JOB_OUTPUT_BYTES = 1 << 20;
|
|
17734
18599
|
var MAX_JOB_OUTPUT_BYTES = 8 << 20;
|
|
18600
|
+
var MAX_JOB_STDIN_BYTES = 1 << 20;
|
|
17735
18601
|
function isAgentJobCapable(transport) {
|
|
17736
18602
|
if (transport === null || typeof transport !== "object") return false;
|
|
17737
18603
|
const candidate = transport;
|
|
@@ -18036,7 +18902,7 @@ function clampInt2(value, fallback, min, max) {
|
|
|
18036
18902
|
}
|
|
18037
18903
|
function remoteJobToolDefinitions(backend) {
|
|
18038
18904
|
return [
|
|
18039
|
-
|
|
18905
|
+
defineTool4({
|
|
18040
18906
|
name: "remote_exec_background",
|
|
18041
18907
|
description: 'Start one shell command on the remote host as a background job and return immediately with its job id, instead of blocking until it finishes. Use it for anything that takes minutes: a full `m -j16` tree build, a test sweep, a long transfer. Read its output, wait for it, list jobs and cancel with `remote_job` (`action: "output" | "status" | "list" | "kill"`) \u2014 that tool also explains how output is retained. Pass `interactive: true` to keep stdin open so `remote_job { action: "stdin" }` can feed it later; without it the job reads EOF immediately.' + // 只保留「作业随连接结束」这一条:它必须出现在**启动**的地方,因为「后台 = 持久」正是这里最
|
|
18042
18908
|
// 容易产生的错误预期。轮询与环形缓冲的细节归 `remote_job`,那里才是读输出的地方。
|
|
@@ -18091,6 +18957,11 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18091
18957
|
const maxOutputBytes = clampInt2(record2["maxOutputBytes"], DEFAULT_JOB_OUTPUT_BYTES, 1, MAX_JOB_OUTPUT_BYTES);
|
|
18092
18958
|
const stdinText = record2["stdin"];
|
|
18093
18959
|
if (stdinText !== void 0 && typeof stdinText !== "string") throw new Error("stdin \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
18960
|
+
if (typeof stdinText === "string" && Buffer.byteLength(stdinText, "utf8") > MAX_JOB_STDIN_BYTES) {
|
|
18961
|
+
throw new Error(
|
|
18962
|
+
`stdin \u4E00\u6B21\u6700\u591A ${MAX_JOB_STDIN_BYTES} \u5B57\u8282\uFF08\u6536\u5230 ${Buffer.byteLength(stdinText, "utf8")}\uFF09\u3002\u8BF7\u7528 interactive:true \u542F\u52A8\uFF0C\u518D\u7528 remote_job {action:"stdin"} \u5206\u5757\u5199\u5165\u3002`
|
|
18963
|
+
);
|
|
18964
|
+
}
|
|
18094
18965
|
const interactive = record2["interactive"] === true;
|
|
18095
18966
|
const target = targetFor2(backend, exec);
|
|
18096
18967
|
const cwd = resolveOrThrow2(target, stringArg2(record2["cwd"]));
|
|
@@ -18114,7 +18985,7 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18114
18985
|
};
|
|
18115
18986
|
}
|
|
18116
18987
|
}),
|
|
18117
|
-
|
|
18988
|
+
defineTool4({
|
|
18118
18989
|
name: "remote_job",
|
|
18119
18990
|
description: "Observe or control a background job started with `remote_exec_background`; `action` decides which: `status` reads one job's state (still running, exit code, how long it ran, bytes produced, bytes dropped); `output` reads its output from a byte offset; `list` shows every job this connection has started, running and finished; `kill` requests termination \u2014 asynchronous, so this reports the request and `status` confirms the end, and the agent kills the whole process group; `stdin` writes to a job started with `interactive: true`, and `close: true` signals EOF; `forget` drops a settled job from the list so it stops crowding `list` (a running job is refused)." + JOB_LIFETIME_NOTE + JOB_POLL_NOTE + JOB_DROP_NOTE + " `list` is per connection: it is not a cross-host task queue.",
|
|
18120
18991
|
parameters: {
|
|
@@ -18210,6 +19081,9 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18210
19081
|
};
|
|
18211
19082
|
}
|
|
18212
19083
|
if (action === "stdin") {
|
|
19084
|
+
if (typeof record2["data"] === "string" && typeof record2["dataBase64"] === "string") {
|
|
19085
|
+
throw new Error("data \u4E0E dataBase64 \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
19086
|
+
}
|
|
18213
19087
|
const result = await backend.stdin(
|
|
18214
19088
|
target,
|
|
18215
19089
|
jobId,
|
|
@@ -18354,7 +19228,7 @@ function registerRemoteJobTools(ctx, backend) {
|
|
|
18354
19228
|
|
|
18355
19229
|
// src/host/integration/fs/remote-file-tools.ts
|
|
18356
19230
|
init_errors();
|
|
18357
|
-
import { defineTool as
|
|
19231
|
+
import { defineTool as defineTool5 } from "@deepseek-ai/dsh-tools";
|
|
18358
19232
|
var DEFAULT_READ_LINES = 2e3;
|
|
18359
19233
|
var MAX_READ_LINES = 5e3;
|
|
18360
19234
|
var DEFAULT_MAX_LINE_LENGTH = 2e3;
|
|
@@ -18428,16 +19302,16 @@ function createRemoteFileBackend(deps) {
|
|
|
18428
19302
|
if (carried !== void 0) rememberVersion(target, to, carried);
|
|
18429
19303
|
},
|
|
18430
19304
|
async stat(target, absolute, signal) {
|
|
18431
|
-
const
|
|
18432
|
-
if (
|
|
19305
|
+
const stat7 = await requireBackend(target).stat(absolute, signal);
|
|
19306
|
+
if (stat7.exists) rememberVersion(target, absolute, stat7.version);
|
|
18433
19307
|
return {
|
|
18434
|
-
exists:
|
|
18435
|
-
isDirectory:
|
|
18436
|
-
isFile:
|
|
18437
|
-
isSymlink:
|
|
18438
|
-
size:
|
|
18439
|
-
mtimeMs:
|
|
18440
|
-
version:
|
|
19308
|
+
exists: stat7.exists,
|
|
19309
|
+
isDirectory: stat7.isDirectory,
|
|
19310
|
+
isFile: stat7.isFile,
|
|
19311
|
+
isSymlink: stat7.isSymlink,
|
|
19312
|
+
size: stat7.size,
|
|
19313
|
+
mtimeMs: stat7.mtimeMs,
|
|
19314
|
+
version: stat7.version
|
|
18441
19315
|
};
|
|
18442
19316
|
},
|
|
18443
19317
|
async patch(target, absolute, diff, expectedVersion, signal) {
|
|
@@ -18566,7 +19440,7 @@ function formatWrite(view) {
|
|
|
18566
19440
|
}
|
|
18567
19441
|
function requiredString3(value, field) {
|
|
18568
19442
|
if (typeof value !== "string" || value.trim() === "") throw new Error(`${field} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
|
|
18569
|
-
return value;
|
|
19443
|
+
return value.trim();
|
|
18570
19444
|
}
|
|
18571
19445
|
function clampInt3(value, fallback, min, max) {
|
|
18572
19446
|
const number = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback;
|
|
@@ -18575,7 +19449,7 @@ function clampInt3(value, fallback, min, max) {
|
|
|
18575
19449
|
}
|
|
18576
19450
|
function remoteFileToolDefinitions(backend, allowMutation) {
|
|
18577
19451
|
return [
|
|
18578
|
-
|
|
19452
|
+
defineTool5({
|
|
18579
19453
|
name: "remote_read",
|
|
18580
19454
|
description: "Read one text file in the remote workspace this session is working in, with **1-based line numbers**, and return a window of it controlled by `offset` (first line) and `limit` (how many lines). Use this instead of `cat` through `remote_exec`: `remote_exec` truncates at 64 KiB and prints no line numbers. The result carries the file's `version` token \u2014 pass it back as `expectedVersion` to `remote_write` or `remote_edit` so a concurrent change is refused instead of overwritten. Read-only: calls to this tool may overlap with other read-only remote calls.",
|
|
18581
19455
|
// Read-only: it observes one remote file and changes nothing.
|
|
@@ -18657,7 +19531,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18657
19531
|
};
|
|
18658
19532
|
}
|
|
18659
19533
|
}),
|
|
18660
|
-
|
|
19534
|
+
defineTool5({
|
|
18661
19535
|
name: "remote_write",
|
|
18662
19536
|
description: "Write one text file in the remote workspace this session is working in, replacing its whole content. Pass `expectedVersion` (from `remote_read`) to make the write refuse when the remote file changed since that read. An existing target without a version guard is **refused** unless `force: true`: a blind overwrite silently discards content this process never saw, including an edit made by another user on a shared machine. Parent directories are created when missing. Pass `mode` to set permission bits in the same call, which is what makes a written build or probe script runnable. NOT concurrency-safe: it changes remote state, so it never joins a parallel group.",
|
|
18663
19537
|
parameters: {
|
|
@@ -18735,7 +19609,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18735
19609
|
};
|
|
18736
19610
|
}
|
|
18737
19611
|
}),
|
|
18738
|
-
|
|
19612
|
+
defineTool5({
|
|
18739
19613
|
name: "remote_edit",
|
|
18740
19614
|
description: "Replace a **literal** string inside one file in the remote workspace this session is working in. `old` is matched verbatim \u2014 a backslash-n in `old` matches the two characters `\\` and `n`, never a newline, so write real newlines when you mean them. `new` is inserted verbatim, so `$&`, `$1` and shell variables in it are not expanded. By default `old` must occur **exactly once**: zero matches and multiple matches are both refused, and the multiple-match refusal reports how many there were. Pass `all: true` to replace every occurrence on purpose. Prefer this over patching through `remote_exec` + python/sed: it cannot mis-escape your text, and `expectedVersion` (from `remote_read`) refuses the edit when the file changed in the meantime. NOT concurrency-safe: it changes remote state, so it never joins a parallel group.",
|
|
18741
19615
|
parameters: {
|
|
@@ -18817,7 +19691,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18817
19691
|
};
|
|
18818
19692
|
}
|
|
18819
19693
|
}),
|
|
18820
|
-
|
|
19694
|
+
defineTool5({
|
|
18821
19695
|
name: "remote_patch",
|
|
18822
19696
|
description: "Apply a **unified diff** to one file in the remote workspace, atomically: every hunk lands in one round trip, and if any hunk fails, nothing is written. Use this instead of calling `remote_edit` three times \u2014 three edits are three read-modify-write cycles, and a failure in the middle leaves the file half changed. Hunks are located by **context, not by line number**: the numbers in `@@ -a,b +c,d @@` are used only in error messages, so a diff generated against a slightly older file still applies as long as the context is right. Each hunk's old text must occur exactly once \u2014 zero occurrences means the file changed, several means the context is not distinctive enough; both are refused rather than guessed at.",
|
|
18823
19697
|
parameters: {
|
|
@@ -18862,7 +19736,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18862
19736
|
return { path, hunks: outcome.hunks, bytes: outcome.bytes, version: outcome.version };
|
|
18863
19737
|
}
|
|
18864
19738
|
}),
|
|
18865
|
-
|
|
19739
|
+
defineTool5({
|
|
18866
19740
|
name: "remote_move",
|
|
18867
19741
|
description: 'Move or rename one path in the remote workspace. `createParents` decides whether a missing destination directory is created (default true) or refused. A cross-device move **fails** rather than silently copying: a silent copy would turn "move then delete the source" into data loss when the delete lands first. Moving onto an existing directory is refused too \u2014 the caller meant a rename, and replacing a directory is not that. Use `remote_copy` when you want a copy that stays put. NOT concurrency-safe: it changes remote state, so it never joins a parallel group.',
|
|
18868
19742
|
parameters: {
|
|
@@ -18893,13 +19767,16 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18893
19767
|
const from = resolveOrThrow3(target, rawFrom);
|
|
18894
19768
|
const to = resolveOrThrow3(target, rawTo);
|
|
18895
19769
|
if (from === to) throw new Error(`\u6E90\u4E0E\u76EE\u6807\u662F\u540C\u4E00\u4E2A\u8DEF\u5F84\uFF1A${from}`);
|
|
19770
|
+
if (to.startsWith(from.endsWith("/") ? from : `${from}/`)) {
|
|
19771
|
+
throw new Error(`\u4E0D\u80FD\u628A\u76EE\u5F55\u79FB\u52A8\u5230\u5B83\u81EA\u5DF1\u7684\u5B50\u76EE\u5F55\u91CC\uFF1A${from} \u2192 ${to}`);
|
|
19772
|
+
}
|
|
18896
19773
|
assertWritable(target, to, allowMutation);
|
|
18897
19774
|
const createParents = record2["createParents"] !== false;
|
|
18898
19775
|
await backend.move(target, from, to, createParents, exec.signal);
|
|
18899
19776
|
return { from, to, createParents };
|
|
18900
19777
|
}
|
|
18901
19778
|
}),
|
|
18902
|
-
|
|
19779
|
+
defineTool5({
|
|
18903
19780
|
name: "remote_stat",
|
|
18904
19781
|
description: 'Read one path\'s **metadata** in the remote workspace without reading its content: whether it exists, whether it is a file, directory or symlink, its size, its modification time and its version token. Use this instead of `remote_read` when you only need to know "does it exist / how big / when did it change" \u2014 reading a file to answer that costs the whole file. The `version` it returns is the same token `remote_read` reports, so it can be passed to `remote_write` / `remote_edit` / `remote_patch` as `expectedVersion`. NOT concurrency-safe: read-only, but it never joins a parallel group.',
|
|
18905
19782
|
parameters: {
|
|
@@ -18929,15 +19806,15 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18929
19806
|
const requested = requiredString3(record2["path"], "path");
|
|
18930
19807
|
const target = targetFor3(backend, exec);
|
|
18931
19808
|
const path = resolveOrThrow3(target, requested);
|
|
18932
|
-
const
|
|
18933
|
-
const kind = !
|
|
19809
|
+
const stat7 = await backend.stat(target, path, exec.signal);
|
|
19810
|
+
const kind = !stat7.exists ? "missing" : stat7.isSymlink ? "symlink" : stat7.isDirectory ? "directory" : "file";
|
|
18934
19811
|
return {
|
|
18935
19812
|
path,
|
|
18936
|
-
exists:
|
|
19813
|
+
exists: stat7.exists,
|
|
18937
19814
|
kind,
|
|
18938
|
-
size:
|
|
18939
|
-
mtimeMs:
|
|
18940
|
-
version:
|
|
19815
|
+
size: stat7.exists ? stat7.size : 0,
|
|
19816
|
+
mtimeMs: stat7.exists ? stat7.mtimeMs : 0,
|
|
19817
|
+
version: stat7.exists ? stat7.version : ""
|
|
18941
19818
|
};
|
|
18942
19819
|
}
|
|
18943
19820
|
})
|
|
@@ -19017,11 +19894,11 @@ var LspFrameParser = class {
|
|
|
19017
19894
|
#append(bytes) {
|
|
19018
19895
|
if (bytes.length === 0) return;
|
|
19019
19896
|
if (this.#end + bytes.length > this.#store.length) {
|
|
19020
|
-
const
|
|
19897
|
+
const pending2 = this.#end - this.#start;
|
|
19021
19898
|
if (this.#start > 0) {
|
|
19022
19899
|
this.#store.copyWithin(0, this.#start, this.#end);
|
|
19023
19900
|
this.#start = 0;
|
|
19024
|
-
this.#end =
|
|
19901
|
+
this.#end = pending2;
|
|
19025
19902
|
}
|
|
19026
19903
|
if (this.#end + bytes.length > this.#store.length) {
|
|
19027
19904
|
let capacity = Math.max(this.#store.length, 64 * 1024);
|
|
@@ -19030,7 +19907,7 @@ var LspFrameParser = class {
|
|
|
19030
19907
|
this.#store.copy(next, 0, this.#start, this.#end);
|
|
19031
19908
|
this.#store = next;
|
|
19032
19909
|
this.#start = 0;
|
|
19033
|
-
this.#end =
|
|
19910
|
+
this.#end = pending2;
|
|
19034
19911
|
}
|
|
19035
19912
|
}
|
|
19036
19913
|
this.#store.set(bytes, this.#end);
|
|
@@ -19242,11 +20119,11 @@ var LspSession = class _LspSession {
|
|
|
19242
20119
|
throw error;
|
|
19243
20120
|
}
|
|
19244
20121
|
const onAbort = () => {
|
|
19245
|
-
const
|
|
19246
|
-
if (
|
|
20122
|
+
const pending2 = this.#pending.get(id);
|
|
20123
|
+
if (pending2 === void 0) return;
|
|
19247
20124
|
this.#pending.delete(id);
|
|
19248
|
-
clearTimeout(
|
|
19249
|
-
|
|
20125
|
+
clearTimeout(pending2.timer);
|
|
20126
|
+
pending2.reject(new LspSessionError("\u7B49\u5F85\u8BED\u8A00\u670D\u52A1\u5668\u65F6\u88AB\u53D6\u6D88"));
|
|
19250
20127
|
};
|
|
19251
20128
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
19252
20129
|
try {
|
|
@@ -19370,15 +20247,15 @@ var LspSession = class _LspSession {
|
|
|
19370
20247
|
if (typeof message["method"] !== "string") {
|
|
19371
20248
|
const id = message["id"];
|
|
19372
20249
|
if (typeof id === "number") {
|
|
19373
|
-
const
|
|
19374
|
-
if (
|
|
20250
|
+
const pending2 = this.#pending.get(id);
|
|
20251
|
+
if (pending2 === void 0) return;
|
|
19375
20252
|
this.#pending.delete(id);
|
|
19376
|
-
clearTimeout(
|
|
20253
|
+
clearTimeout(pending2.timer);
|
|
19377
20254
|
const error = message["error"];
|
|
19378
20255
|
if (error !== void 0 && error !== null) {
|
|
19379
|
-
|
|
20256
|
+
pending2.reject(new LspSessionError(`${pending2.method} \u5931\u8D25\uFF1A${describeError(error)}`));
|
|
19380
20257
|
} else {
|
|
19381
|
-
|
|
20258
|
+
pending2.resolve(message["result"]);
|
|
19382
20259
|
}
|
|
19383
20260
|
}
|
|
19384
20261
|
return;
|
|
@@ -19434,10 +20311,10 @@ var LspSession = class _LspSession {
|
|
|
19434
20311
|
}
|
|
19435
20312
|
#failAll(error) {
|
|
19436
20313
|
this.#exitReason ??= error;
|
|
19437
|
-
for (const [id,
|
|
19438
|
-
clearTimeout(
|
|
20314
|
+
for (const [id, pending2] of this.#pending) {
|
|
20315
|
+
clearTimeout(pending2.timer);
|
|
19439
20316
|
this.#pending.delete(id);
|
|
19440
|
-
|
|
20317
|
+
pending2.reject(error);
|
|
19441
20318
|
}
|
|
19442
20319
|
}
|
|
19443
20320
|
};
|
|
@@ -19601,8 +20478,12 @@ function describeInstalled(available) {
|
|
|
19601
20478
|
return `\u8BE5\u4E3B\u673A\u4E0A\u63A2\u6D4B\u5230\u7684\u8BED\u8A00\u670D\u52A1\u5668\uFF1A${list}\u3002`;
|
|
19602
20479
|
}
|
|
19603
20480
|
var MAX_SESSIONS = 8;
|
|
19604
|
-
function
|
|
19605
|
-
return `${target.connectionId}\0${target.root}\0${language}`;
|
|
20481
|
+
function sessionKeyPrefix(target, language) {
|
|
20482
|
+
return `${target.connectionId}\0${target.root}\0${language}\0`;
|
|
20483
|
+
}
|
|
20484
|
+
function sessionKey(target, language, override) {
|
|
20485
|
+
const server = override === void 0 ? "" : `${override.command}\0${(override.args ?? []).join("\0")}`;
|
|
20486
|
+
return `${sessionKeyPrefix(target, language)}${server}`;
|
|
19606
20487
|
}
|
|
19607
20488
|
function createLspRegistry(deps) {
|
|
19608
20489
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -19621,15 +20502,15 @@ function createLspRegistry(deps) {
|
|
|
19621
20502
|
}
|
|
19622
20503
|
return {
|
|
19623
20504
|
async session(target, language, override, signal) {
|
|
19624
|
-
const key = sessionKey(target, language);
|
|
20505
|
+
const key = sessionKey(target, language, override);
|
|
19625
20506
|
const existing = sessions.get(key);
|
|
19626
20507
|
if (existing !== void 0 && !existing.session.stopped) {
|
|
19627
20508
|
sessions.delete(key);
|
|
19628
20509
|
sessions.set(key, existing);
|
|
19629
20510
|
return existing.session;
|
|
19630
20511
|
}
|
|
19631
|
-
const
|
|
19632
|
-
if (
|
|
20512
|
+
const pending2 = inFlight.get(key);
|
|
20513
|
+
if (pending2 !== void 0) return pending2;
|
|
19633
20514
|
const attempt = (async () => {
|
|
19634
20515
|
const command = override ?? defaultServerFor(language);
|
|
19635
20516
|
if (command === void 0) {
|
|
@@ -19701,17 +20582,21 @@ ${describeInstalled(available)}`);
|
|
|
19701
20582
|
}));
|
|
19702
20583
|
},
|
|
19703
20584
|
async stop(target, language, signal) {
|
|
19704
|
-
const
|
|
19705
|
-
const
|
|
19706
|
-
if (
|
|
19707
|
-
|
|
19708
|
-
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
|
|
19714
|
-
|
|
20585
|
+
const prefix = sessionKeyPrefix(target, language);
|
|
20586
|
+
const keys = [...new Set([...sessions.keys(), ...inFlight.keys()].filter((key) => key.startsWith(prefix)))];
|
|
20587
|
+
if (keys.length === 0) return false;
|
|
20588
|
+
for (const key of keys) {
|
|
20589
|
+
const starting = inFlight.get(key);
|
|
20590
|
+
if (starting !== void 0) {
|
|
20591
|
+
retired.add(key);
|
|
20592
|
+
await starting.catch(() => void 0);
|
|
20593
|
+
retired.delete(key);
|
|
20594
|
+
}
|
|
20595
|
+
const entry = sessions.get(key);
|
|
20596
|
+
if (entry === void 0) continue;
|
|
20597
|
+
sessions.delete(key);
|
|
20598
|
+
await entry.session.close(signal);
|
|
20599
|
+
}
|
|
19715
20600
|
return true;
|
|
19716
20601
|
},
|
|
19717
20602
|
async disposeAll() {
|
|
@@ -19726,7 +20611,7 @@ ${describeInstalled(available)}`);
|
|
|
19726
20611
|
|
|
19727
20612
|
// src/host/integration/tools/forward-tools.ts
|
|
19728
20613
|
init_errors();
|
|
19729
|
-
import { defineTool as
|
|
20614
|
+
import { defineTool as defineTool6 } from "@deepseek-ai/dsh-tools";
|
|
19730
20615
|
function createForwardToolBackend(acquire) {
|
|
19731
20616
|
const records = /* @__PURE__ */ new Map();
|
|
19732
20617
|
let sequence2 = 0;
|
|
@@ -19788,7 +20673,7 @@ function text5(value) {
|
|
|
19788
20673
|
return [{ type: "text", text: value }];
|
|
19789
20674
|
}
|
|
19790
20675
|
function remoteForwardDefinition(backend) {
|
|
19791
|
-
return
|
|
20676
|
+
return defineTool6({
|
|
19792
20677
|
name: "remote_forward",
|
|
19793
20678
|
description: "Forward one remote TCP port to a local one, so a service the agent started on the remote host becomes reachable from this machine (and from the DSH browser sidebar). Typical use: start a dev server with `remote_exec_background`, then `remote_forward` its port and open the returned `http://127.0.0.1:<port>` URL. The local listener binds **127.0.0.1 only** \u2014 never the LAN; open it up via the host sshd's `GatewayPorts` if that is really what you want. `start` returns the real local port (auto-picked when omitted) plus a `forwardId`; `list` shows what is live; `stop` closes one and is idempotent. Forwards live as long as the connection and are closed automatically when the plugin unloads, so a forgotten one does not leak a port forever \u2014 but stop the ones you are done with.",
|
|
19794
20679
|
parameters: {
|
|
@@ -19891,7 +20776,7 @@ function registerRemoteForwardTool(ctx, backend) {
|
|
|
19891
20776
|
|
|
19892
20777
|
// src/host/integration/tools/remote-git-tools.ts
|
|
19893
20778
|
init_shell();
|
|
19894
|
-
import { defineTool as
|
|
20779
|
+
import { defineTool as defineTool7 } from "@deepseek-ai/dsh-tools";
|
|
19895
20780
|
var READ_ACTIONS = ["status", "diff", "log", "show", "branch"];
|
|
19896
20781
|
var WRITE_ACTIONS = ["add", "commit", "checkout", "restore", "stash"];
|
|
19897
20782
|
var ACTIONS = [...READ_ACTIONS, ...WRITE_ACTIONS];
|
|
@@ -20078,7 +20963,7 @@ function formatOutcome(value) {
|
|
|
20078
20963
|
${body}${suffix}`;
|
|
20079
20964
|
}
|
|
20080
20965
|
function remoteGitDefinition(backend) {
|
|
20081
|
-
return
|
|
20966
|
+
return defineTool7({
|
|
20082
20967
|
name: "remote_git",
|
|
20083
20968
|
description: "Run a git operation on the remote repository this session is working in, and return its **parsed** result instead of raw terminal text. Prefer this over `remote_exec git ... : the parse is done here (branch/ahead/behind, status entries, log entries), so a new git version's wording cannot break your reading. Read-only actions: `status`, `diff`, `log`, `show`, `branch`. Mutating actions: `add`, `commit`, `checkout`, `restore`, `stash` \u2014 these change the user's repository, so state the intent in your message first. Deliberately **not** offered: `reset --hard`, `clean -fd`, `push --force`, `checkout .`. Those have no safe preview; use `remote_exec` explicitly if you really need one. Git is run with `GIT_TERMINAL_PROMPT=0`, so a missing credential **fails** instead of hanging on a password prompt. Read-only actions may overlap with other read-only remote calls.",
|
|
20084
20969
|
parameters: {
|
|
@@ -20252,7 +21137,7 @@ function registerRemoteGitTool(ctx, backend) {
|
|
|
20252
21137
|
}
|
|
20253
21138
|
|
|
20254
21139
|
// src/host/integration/tools/remote-lsp-tools.ts
|
|
20255
|
-
import { defineTool as
|
|
21140
|
+
import { defineTool as defineTool8 } from "@deepseek-ai/dsh-tools";
|
|
20256
21141
|
function toView(diagnostic) {
|
|
20257
21142
|
return {
|
|
20258
21143
|
line: diagnostic.line + 1,
|
|
@@ -20304,7 +21189,7 @@ function parseServer(value) {
|
|
|
20304
21189
|
function remoteLspToolDefinitions(deps) {
|
|
20305
21190
|
const { backend, registry } = deps;
|
|
20306
21191
|
return [
|
|
20307
|
-
|
|
21192
|
+
defineTool8({
|
|
20308
21193
|
name: "remote_lsp",
|
|
20309
21194
|
description: 'Ask a **language server running on the remote host** about a file: diagnostics, hover, or definition. Use this instead of running the compiler and reading its text output \u2014 the answer is structured and scoped to one document, and it is the only way to ask "what does this symbol mean" or "where is this defined". The server is started on first use and reused; it takes seconds to index a project, so the first call is slow and later ones are fast. Line and column are **1-based**, matching `remote_read`. Requires a language server installed on the remote host (gopls, rust-analyzer, typescript-language-server, pylsp, clangd); pass `server` to use a different one. NOT concurrency-safe: it changes remote state (starts a process), so it never joins a parallel group.',
|
|
20310
21195
|
parameters: {
|
|
@@ -20513,11 +21398,10 @@ function registerRemoteLspTools(ctx, deps) {
|
|
|
20513
21398
|
// src/wiring/tools-session.ts
|
|
20514
21399
|
import { cp, mkdir as mkdir10, readFile as readFile9, readdir as readdir6, rename as rename6, rm as rm5, stat as nodeStat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
20515
21400
|
import { dirname as dirname8, join as join17 } from "node:path";
|
|
20516
|
-
import { homedir as homedir6 } from "node:os";
|
|
20517
21401
|
|
|
20518
21402
|
// src/host/integration/session/session-labels.ts
|
|
20519
21403
|
function normalizePath(path) {
|
|
20520
|
-
return path
|
|
21404
|
+
return foldPathKey(path);
|
|
20521
21405
|
}
|
|
20522
21406
|
function createSessionTitleResolver(ctx) {
|
|
20523
21407
|
return (header) => {
|
|
@@ -20563,8 +21447,8 @@ function createWorkspaceTitleResolver(ctx) {
|
|
|
20563
21447
|
// src/wiring/tools-session.ts
|
|
20564
21448
|
function wireSessionAdmin(env) {
|
|
20565
21449
|
const { ctx, limits, notifySessionRemoved: notifySessionRemoved2 } = env;
|
|
20566
|
-
const sessionsRoot = limits.sessionsRoot ?? join17(
|
|
20567
|
-
const storagesRoot = join17(
|
|
21450
|
+
const sessionsRoot = limits.sessionsRoot ?? join17(dshHome(), "sessions");
|
|
21451
|
+
const storagesRoot = join17(dshHome(), "storages");
|
|
20568
21452
|
const sessionBinRoot = limits.sessionBinRoot ?? join17(storagesRoot, "over-ssh", "session-trash");
|
|
20569
21453
|
if (limits.sessionBinRoot === void 0) {
|
|
20570
21454
|
const carried = carryDirectory(join17(storagesRoot, "remote-workspace", "session-trash"), sessionBinRoot);
|
|
@@ -20587,33 +21471,47 @@ function wireSessionAdmin(env) {
|
|
|
20587
21471
|
listDir: async (path) => {
|
|
20588
21472
|
try {
|
|
20589
21473
|
const entries = await readdir6(path, { withFileTypes: true });
|
|
20590
|
-
const
|
|
20591
|
-
|
|
20592
|
-
|
|
20593
|
-
|
|
20594
|
-
|
|
20595
|
-
const
|
|
20596
|
-
|
|
20597
|
-
if (
|
|
20598
|
-
|
|
21474
|
+
const statLimit = 16;
|
|
21475
|
+
const out = new Array(entries.length);
|
|
21476
|
+
let next = 0;
|
|
21477
|
+
const workers = Array.from({ length: Math.max(1, Math.min(statLimit, entries.length)) }, async () => {
|
|
21478
|
+
for (; ; ) {
|
|
21479
|
+
const index = next;
|
|
21480
|
+
next += 1;
|
|
21481
|
+
if (index >= entries.length) return;
|
|
21482
|
+
const entry = entries[index];
|
|
21483
|
+
if (entry === void 0) return;
|
|
21484
|
+
let bytes = 0;
|
|
21485
|
+
let mtimeMs = 0;
|
|
21486
|
+
try {
|
|
21487
|
+
const info = await nodeStat2(join17(path, entry.name));
|
|
21488
|
+
mtimeMs = info.mtimeMs;
|
|
21489
|
+
if (entry.isFile()) bytes = info.size;
|
|
21490
|
+
} catch {
|
|
21491
|
+
}
|
|
21492
|
+
out[index] = { name: entry.name, isDirectory: entry.isDirectory(), bytes, mtimeMs };
|
|
20599
21493
|
}
|
|
20600
|
-
|
|
20601
|
-
|
|
21494
|
+
});
|
|
21495
|
+
await Promise.all(workers);
|
|
20602
21496
|
return out;
|
|
20603
21497
|
} catch (error) {
|
|
20604
21498
|
if (error.code === "ENOENT") return [];
|
|
20605
21499
|
throw error;
|
|
20606
21500
|
}
|
|
20607
21501
|
},
|
|
20608
|
-
|
|
21502
|
+
// The listing is already in hand: hand it over so the header cache can key on it instead of re-listing.
|
|
21503
|
+
readHeader: (dir, listing) => readSessionHeader(
|
|
21504
|
+
dir,
|
|
21505
|
+
listing?.map((entry) => ({ name: entry.name, size: entry.bytes, mtimeMs: entry.mtimeMs }))
|
|
21506
|
+
),
|
|
20609
21507
|
// Both resolvers are optional: a profile without the platform services still lists sessions, labelled
|
|
20610
21508
|
// by their directory and id rather than by name.
|
|
20611
21509
|
resolveSessionTitle: createSessionTitleResolver(ctx),
|
|
20612
21510
|
resolveWorkspaceTitle: createWorkspaceTitleResolver(ctx),
|
|
20613
21511
|
stat: async (path) => {
|
|
20614
21512
|
try {
|
|
20615
|
-
const
|
|
20616
|
-
return { isDirectory:
|
|
21513
|
+
const stat7 = await nodeStat2(path);
|
|
21514
|
+
return { isDirectory: stat7.isDirectory(), bytes: stat7.size };
|
|
20617
21515
|
} catch {
|
|
20618
21516
|
return void 0;
|
|
20619
21517
|
}
|
|
@@ -20690,7 +21588,7 @@ init_errors();
|
|
|
20690
21588
|
|
|
20691
21589
|
// src/host/integration/tools/management-tools.ts
|
|
20692
21590
|
import { isAbsolute as isAbsolute3, resolve as resolve2 } from "node:path";
|
|
20693
|
-
import { defineTool as
|
|
21591
|
+
import { defineTool as defineTool9 } from "@deepseek-ai/dsh-tools";
|
|
20694
21592
|
function text7(value) {
|
|
20695
21593
|
return [{ type: "text", text: value }];
|
|
20696
21594
|
}
|
|
@@ -20765,7 +21663,7 @@ function formatEnv(result) {
|
|
|
20765
21663
|
}
|
|
20766
21664
|
function managementToolDefinitions(backend) {
|
|
20767
21665
|
return [
|
|
20768
|
-
|
|
21666
|
+
defineTool9({
|
|
20769
21667
|
name: "remote_transfer",
|
|
20770
21668
|
description: "Upload one local file to the remote workspace this session is working in, or download one remote file to the local machine. Chunked with progress; pass `resume: true` to continue an interrupted transfer instead of rewriting the whole file (the prefix is **compared with the source** before it is kept, so a changed source degrades to a full retransfer instead of splicing two files). The result reports bytes, chunk count, whether it resumed, the absolute local path the bytes landed at, and \u2014 when the host can hash \u2014 the digest of the content that actually landed. `localPath` must be **absolute**: the plugin resolves it in its own process, so a relative spelling would land under the plugin's working directory instead of anywhere the caller expects. An upload that would overwrite an **existing** remote file is refused unless you pass `expectedVersion` (from `remote_stat`/`remote_read`) or `force: true`: a silent overwrite is the one operation that destroys someone else's work without a trace.",
|
|
20771
21669
|
parameters: {
|
|
@@ -20854,6 +21752,7 @@ function managementToolDefinitions(backend) {
|
|
|
20854
21752
|
throw new Error("expectedVersion / expectedSha256 / force \u53EA\u5BF9 upload \u6709\u610F\u4E49\uFF1B\u4E0B\u8F7D\u7531\u672C\u5730\u6587\u4EF6\u7CFB\u7EDF\u8986\u76D6\uFF0C\u4E0D\u505A\u8FDC\u7AEF\u5B88\u536B");
|
|
20855
21753
|
}
|
|
20856
21754
|
const target = targetFor4(backend, exec);
|
|
21755
|
+
if (direction === "upload") backend.assertWritable?.(target, "\u4E0A\u4F20\u8FDC\u7AEF\u6587\u4EF6");
|
|
20857
21756
|
if (direction === "upload" && backend.statRemote !== void 0 && (expectedVersion !== void 0 || !force && expectedSha256 === void 0)) {
|
|
20858
21757
|
const absolute = resolveUnderRoot(target.root, remotePath);
|
|
20859
21758
|
if (typeof absolute !== "string") throw new Error(absolute.error);
|
|
@@ -20903,7 +21802,7 @@ function managementToolDefinitions(backend) {
|
|
|
20903
21802
|
};
|
|
20904
21803
|
}
|
|
20905
21804
|
}),
|
|
20906
|
-
|
|
21805
|
+
defineTool9({
|
|
20907
21806
|
name: "remote_delete",
|
|
20908
21807
|
description: 'Delete one path inside the remote workspace. `mode: "recycle"` (the default) moves it to the workspace recycle bin, where it is recoverable and can be undone; `mode: "permanent"` removes it for good and requires user approval. Omitting `mode` is therefore the safe call, and the result always names which mode ran.',
|
|
20909
21808
|
parameters: {
|
|
@@ -20941,6 +21840,7 @@ function managementToolDefinitions(backend) {
|
|
|
20941
21840
|
const target = targetFor4(backend, exec);
|
|
20942
21841
|
const resolved = resolveUnderRoot(target.root, requested, void 0, target.connectionId);
|
|
20943
21842
|
if (typeof resolved !== "string") throw new Error(resolved.error);
|
|
21843
|
+
backend.assertWritable?.(target, mode === "permanent" ? "\u6C38\u4E45\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6" : "\u5220\u9664\u8FDC\u7AEF\u6587\u4EF6");
|
|
20944
21844
|
const result = await backend.discard({ target, path: resolved, mode, signal: exec.signal });
|
|
20945
21845
|
return {
|
|
20946
21846
|
mode: result.mode,
|
|
@@ -20951,7 +21851,7 @@ function managementToolDefinitions(backend) {
|
|
|
20951
21851
|
};
|
|
20952
21852
|
}
|
|
20953
21853
|
}),
|
|
20954
|
-
|
|
21854
|
+
defineTool9({
|
|
20955
21855
|
name: "remote_restore",
|
|
20956
21856
|
description: "Undo a delete. With `id`, puts that bin entry back at the path it was deleted from; without `id`, undoes the most recent discard; with `list: true`, shows what the bin holds for this session's root and changes nothing. `remote_delete` returns the entry id as `entryId`. The bin is subject to retention limits (`binMaxEntries` / `binMaxBytes` / `binMaxAgeMs`), so an old entry may already be gone.",
|
|
20957
21857
|
parameters: {
|
|
@@ -21000,6 +21900,8 @@ function managementToolDefinitions(backend) {
|
|
|
21000
21900
|
throw new Error("id \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u7701\u7565\u5B83\u8868\u793A\u64A4\u9500\u6700\u8FD1\u4E00\u6B21\u5220\u9664\uFF09");
|
|
21001
21901
|
}
|
|
21002
21902
|
const target = targetFor4(backend, exec);
|
|
21903
|
+
const listing = record2["list"] === true;
|
|
21904
|
+
if (!listing) backend.assertWritable?.(target, "\u4ECE\u56DE\u6536\u7AD9\u6062\u590D\u8FDC\u7AEF\u6587\u4EF6");
|
|
21003
21905
|
const outcome = await backend.restore({
|
|
21004
21906
|
target,
|
|
21005
21907
|
id: typeof id === "string" ? id : void 0,
|
|
@@ -21013,7 +21915,7 @@ function managementToolDefinitions(backend) {
|
|
|
21013
21915
|
return { action: "undone", id: outcome.id, message: outcome.message };
|
|
21014
21916
|
}
|
|
21015
21917
|
}),
|
|
21016
|
-
|
|
21918
|
+
defineTool9({
|
|
21017
21919
|
name: "remote_copy",
|
|
21018
21920
|
description: "Copy one file to another path **on the remote host**. Both paths are inside this session's remote root. On a Tier 2 (agent) connection the copy happens on the remote side, so the bytes never cross this machine; otherwise they are relayed through it, and the result says which happened in `via`. Pass `recursive: true` to copy a whole directory tree in one round trip (Tier 2 only \u2014 it refuses rather than relaying a tree file by file). The destination must not exist, or be an empty directory: a recursive copy never deletes anything.",
|
|
21019
21921
|
parameters: {
|
|
@@ -21056,6 +21958,7 @@ function managementToolDefinitions(backend) {
|
|
|
21056
21958
|
if (typeof source !== "string") throw new Error(source.error);
|
|
21057
21959
|
const destination = resolveUnderRoot(target.root, to, void 0, target.connectionId);
|
|
21058
21960
|
if (typeof destination !== "string") throw new Error(destination.error);
|
|
21961
|
+
backend.assertWritable?.(target, "\u590D\u5236\u8FDC\u7AEF\u6587\u4EF6");
|
|
21059
21962
|
const outcome = await backend.copy({
|
|
21060
21963
|
target,
|
|
21061
21964
|
from: source,
|
|
@@ -21076,7 +21979,7 @@ function managementToolDefinitions(backend) {
|
|
|
21076
21979
|
};
|
|
21077
21980
|
}
|
|
21078
21981
|
}),
|
|
21079
|
-
|
|
21982
|
+
defineTool9({
|
|
21080
21983
|
name: "remote_env",
|
|
21081
21984
|
description: "Sample what the remote host is and what it can do: OS/arch/distro/kernel, shell, CPU count, memory and disk, user and home, and **which commands exist** (rg, fd, git, python3, gcc, docker, ...). Ask this once instead of guessing and retrying: the answer is cached on the agent until `refresh: true`, so a repeat call costs one round trip and no resampling.",
|
|
21082
21985
|
parameters: {
|
|
@@ -21290,6 +22193,8 @@ function wireManagementTools(env) {
|
|
|
21290
22193
|
)?.root;
|
|
21291
22194
|
const managementBackend = {
|
|
21292
22195
|
resolve: (cwd) => toolBackend.resolve(cwd),
|
|
22196
|
+
// 管理工具(上传/删除/恢复/复制)之前完全不过写闸:read-only 会话照样能删文件(审计 2026-11-06 宿主 P0)。
|
|
22197
|
+
assertWritable: (target, action) => env.assertWritable(target, action),
|
|
21293
22198
|
// ADR-0013: the session id (plus its fork lineage) is the strict scope; `cwd` alone cannot separate
|
|
21294
22199
|
// two conversations opened on the same directory.
|
|
21295
22200
|
resolveSessionRoot: (cwd, sessionId) => sessionRoots.outcomeFor(cwd, sessionIdentityOf(ctx, sessionId)),
|
|
@@ -21409,6 +22314,7 @@ function wireManagementTools(env) {
|
|
|
21409
22314
|
}
|
|
21410
22315
|
|
|
21411
22316
|
// src/wiring/tools.ts
|
|
22317
|
+
init_errors();
|
|
21412
22318
|
function sessionRootsFilePath() {
|
|
21413
22319
|
return join18(resolveStateDir(), USER_ROOTS_FILE_NAME);
|
|
21414
22320
|
}
|
|
@@ -21449,7 +22355,8 @@ function wireTools(env) {
|
|
|
21449
22355
|
disposeJobTools,
|
|
21450
22356
|
disposeFileTools,
|
|
21451
22357
|
lspRegistry,
|
|
21452
|
-
disposeLspTools
|
|
22358
|
+
disposeLspTools,
|
|
22359
|
+
assertWritable: assertWritable2
|
|
21453
22360
|
} = wireFileAndJobTools({ ctx, limits, profiles, transports, registry, sessionRoots });
|
|
21454
22361
|
const sessionAdmin = wireSessionAdmin({ ctx, limits, notifySessionRemoved: notifySessionRemoved2 });
|
|
21455
22362
|
const { dispose: disposeManagement } = wireManagementTools({
|
|
@@ -21463,7 +22370,8 @@ function wireTools(env) {
|
|
|
21463
22370
|
sessionRoots,
|
|
21464
22371
|
recordFor,
|
|
21465
22372
|
transferService,
|
|
21466
|
-
transferTracker
|
|
22373
|
+
transferTracker,
|
|
22374
|
+
assertWritable: assertWritable2
|
|
21467
22375
|
});
|
|
21468
22376
|
const disposeStrictArguments = strictArgumentsGate(ctx);
|
|
21469
22377
|
return {
|
|
@@ -21532,7 +22440,9 @@ function wireAgentTools(env) {
|
|
|
21532
22440
|
disposeRemoteTools();
|
|
21533
22441
|
disposeGitTool();
|
|
21534
22442
|
disposeForwardTool();
|
|
21535
|
-
void forwardBackend.closeAll()
|
|
22443
|
+
void forwardBackend.closeAll().catch((error) => {
|
|
22444
|
+
ctx.logger?.warn?.(`over-ssh: \u5173\u95ED\u7AEF\u53E3\u8F6C\u53D1\u65F6\u51FA\u9519\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
22445
|
+
});
|
|
21536
22446
|
};
|
|
21537
22447
|
return { sessionRoots, toolBackend, disposeTools };
|
|
21538
22448
|
}
|
|
@@ -21569,21 +22479,33 @@ function wireFileAndJobTools(env) {
|
|
|
21569
22479
|
backendFor: (target) => recordFor(target)?.backend,
|
|
21570
22480
|
...limits.maxReadBytes === void 0 ? {} : { maxReadBytes: limits.maxReadBytes }
|
|
21571
22481
|
});
|
|
21572
|
-
const allowMutation = (target) => {
|
|
22482
|
+
const allowMutation = (target, action) => {
|
|
22483
|
+
const policy = platformMutationPolicy(ctx);
|
|
22484
|
+
const sessionId = currentSessionId();
|
|
22485
|
+
const cwd = sessionId === void 0 || sessionId === "" ? void 0 : readSessionCwd(ctx, sessionId);
|
|
21573
22486
|
const refusal = mutationRefusal(
|
|
21574
|
-
`${target.connectionId}:${target.root}`,
|
|
22487
|
+
action === void 0 ? `${target.connectionId}:${target.root}` : `${action}\uFF1A${target.connectionId}:${target.root}`,
|
|
21575
22488
|
{
|
|
21576
|
-
...
|
|
22489
|
+
...policy,
|
|
21577
22490
|
rootArea: () => {
|
|
21578
22491
|
const record2 = recordFor(target);
|
|
21579
22492
|
if (record2 === void 0) return "deny";
|
|
21580
|
-
return workspaceWriteArea(record2.root,
|
|
22493
|
+
return workspaceWriteArea(record2.root, policy.policy?.workspaceRoot, cwd, sessionIdentityOf(ctx, sessionId));
|
|
21581
22494
|
}
|
|
21582
22495
|
}
|
|
21583
22496
|
);
|
|
21584
22497
|
if (refusal !== void 0) throw new Error(refusal);
|
|
21585
22498
|
};
|
|
21586
22499
|
const disposeFileTools = registerRemoteFileTools(ctx, fileBackend, { allowMutation });
|
|
22500
|
+
const assertWritable2 = (target, action) => {
|
|
22501
|
+
allowMutation(target, action);
|
|
22502
|
+
if (recordFor(target)?.root.writable === false) {
|
|
22503
|
+
throw new SshDevError("SANDBOX_DENIED", `\u8FD9\u4E2A\u6839\u5728\u914D\u7F6E\u91CC\u662F\u53EA\u8BFB\u7684\uFF08writable: false\uFF09\uFF0C\u62D2\u7EDD\u5199\u5165\uFF1A${action}`, {
|
|
22504
|
+
connectionId: target.connectionId,
|
|
22505
|
+
root: target.root
|
|
22506
|
+
});
|
|
22507
|
+
}
|
|
22508
|
+
};
|
|
21587
22509
|
const lspRegistry = createLspRegistry({
|
|
21588
22510
|
acquire: async (connectionId) => (await transports.acquire(requireProfile(profiles, connectionId))).transport,
|
|
21589
22511
|
onLog: (connectionId, message) => {
|
|
@@ -21591,7 +22513,7 @@ function wireFileAndJobTools(env) {
|
|
|
21591
22513
|
}
|
|
21592
22514
|
});
|
|
21593
22515
|
const disposeLspTools = registerRemoteLspTools(ctx, { backend: fileBackend, registry: lspRegistry });
|
|
21594
|
-
return { recordFor, disposeJobTools, disposeFileTools, lspRegistry, disposeLspTools };
|
|
22516
|
+
return { recordFor, disposeJobTools, disposeFileTools, lspRegistry, disposeLspTools, assertWritable: assertWritable2 };
|
|
21595
22517
|
}
|
|
21596
22518
|
|
|
21597
22519
|
// src/index.ts
|