dsh-deeppilot 0.6.2 → 0.7.1
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/COMPATIBILITY.md +44 -5
- package/README.md +37 -21
- package/README.zh-CN.md +25 -14
- package/SECURITY.md +1 -1
- package/lib/client.js +287 -166
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +34 -4
- package/lib/index.js +437 -116
- package/lib/index.js.map +1 -1
- package/package.json +19 -16
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, sign, timingSafeEqual, verify } from "node:crypto";
|
|
2
|
+
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, sign, timingSafeEqual, verify } from "node:crypto";
|
|
3
3
|
import { access, chmod, mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
@@ -11,10 +11,11 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
11
11
|
import { connect } from "node:http2";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { request } from "node:https";
|
|
14
|
+
import { createServer, request } from "node:https";
|
|
15
15
|
import z from "@deepseek-ai/schemastery";
|
|
16
16
|
import { isIP } from "node:net";
|
|
17
|
-
import { createServer } from "node:http";
|
|
17
|
+
import { createServer as createServer$1 } from "node:http";
|
|
18
|
+
import { generate } from "selfsigned";
|
|
18
19
|
function validSendId(id) {
|
|
19
20
|
return typeof id === "string" && /^\d{13}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
20
21
|
}
|
|
@@ -644,7 +645,7 @@ function requiredScope(type) {
|
|
|
644
645
|
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
645
646
|
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") return "notifications.register";
|
|
646
647
|
if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
|
|
647
|
-
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
648
|
+
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.unarchive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
648
649
|
if (type.startsWith("c2s.")) return "sessions.read";
|
|
649
650
|
}
|
|
650
651
|
/** Broadcast and replay authorization. Unknown frame types fail closed. */
|
|
@@ -1024,6 +1025,18 @@ var BridgeConnection = class {
|
|
|
1024
1025
|
this.send("s2c.session.archived", { sessionId: p.sessionId }, env.id);
|
|
1025
1026
|
return;
|
|
1026
1027
|
}
|
|
1028
|
+
case "c2s.sessions.archived":
|
|
1029
|
+
this.send("s2c.sessions.archived.snapshot", { sessions: this.deps.bridge.listArchivedSessions() }, env.id);
|
|
1030
|
+
return;
|
|
1031
|
+
case "c2s.session.unarchive": {
|
|
1032
|
+
const p = env.payload;
|
|
1033
|
+
if (!p?.sessionId) return this.fail(env.id, "E_PROTOCOL", "sessionId required");
|
|
1034
|
+
if (!this.deps.bridge.capabilities.sessionRestore) return this.fail(env.id, "E_UNSUPPORTED", "session restore unavailable on this host version");
|
|
1035
|
+
const result = await this.deps.bridge.unarchiveSession(p.sessionId);
|
|
1036
|
+
if (!result.ok) return this.fail(env.id, managementErrorCode(result.kind), result.message);
|
|
1037
|
+
this.send("s2c.session.unarchived", { sessionId: p.sessionId }, env.id);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1027
1040
|
case "c2s.session.cancel": {
|
|
1028
1041
|
const p = env.payload;
|
|
1029
1042
|
if (!p?.sessionId) return this.fail(env.id, "E_PROTOCOL", "sessionId required");
|
|
@@ -1434,14 +1447,28 @@ function projectEvent(sessionId, event) {
|
|
|
1434
1447
|
}
|
|
1435
1448
|
case "tool/result": {
|
|
1436
1449
|
const data = event.data;
|
|
1450
|
+
const ok = data?.error === void 0;
|
|
1451
|
+
const result = limitMessageProjection({
|
|
1452
|
+
seq: event.seq,
|
|
1453
|
+
role: "tool",
|
|
1454
|
+
ts: tsOf(event),
|
|
1455
|
+
tool: {
|
|
1456
|
+
name: "result",
|
|
1457
|
+
state: ok ? "ok" : "error",
|
|
1458
|
+
summary: ok ? summarizeResult(data?.message?.content) : "失败"
|
|
1459
|
+
},
|
|
1460
|
+
...ok ? attachmentProjection(data?.message) : {}
|
|
1461
|
+
});
|
|
1437
1462
|
return {
|
|
1438
1463
|
kind: "tool.end",
|
|
1439
1464
|
data: {
|
|
1440
|
-
seq:
|
|
1441
|
-
role:
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1465
|
+
seq: result.seq,
|
|
1466
|
+
role: result.role,
|
|
1467
|
+
ts: result.ts,
|
|
1468
|
+
ok,
|
|
1469
|
+
summary: result.tool?.summary,
|
|
1470
|
+
...result.attachments ? { attachments: result.attachments } : {},
|
|
1471
|
+
...data?.callId ? { callId: String(data.callId) } : {}
|
|
1445
1472
|
}
|
|
1446
1473
|
};
|
|
1447
1474
|
}
|
|
@@ -1679,19 +1706,23 @@ function projectHistory(events) {
|
|
|
1679
1706
|
const target = callId ? toolByCall.get(callId) : void 0;
|
|
1680
1707
|
const failed = data?.error !== void 0;
|
|
1681
1708
|
const summary = failed ? "失败" : summarizeResult(data?.message?.content);
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1709
|
+
const attachments = failed ? {} : attachmentProjection(data?.message);
|
|
1710
|
+
if (target?.tool) {
|
|
1711
|
+
target.tool = {
|
|
1712
|
+
...target.tool,
|
|
1713
|
+
state: failed ? "error" : "ok",
|
|
1714
|
+
summary
|
|
1715
|
+
};
|
|
1716
|
+
if (attachments.attachments) Object.assign(target, attachments);
|
|
1717
|
+
} else messages.push({
|
|
1688
1718
|
...base,
|
|
1689
1719
|
role: "tool",
|
|
1690
1720
|
tool: {
|
|
1691
1721
|
name: "result",
|
|
1692
1722
|
state: failed ? "error" : "ok",
|
|
1693
1723
|
summary
|
|
1694
|
-
}
|
|
1724
|
+
},
|
|
1725
|
+
...attachments
|
|
1695
1726
|
});
|
|
1696
1727
|
break;
|
|
1697
1728
|
}
|
|
@@ -1815,9 +1846,13 @@ var HostBridge = class {
|
|
|
1815
1846
|
historyBufferMax;
|
|
1816
1847
|
id = ++BRIDGE_SEQ;
|
|
1817
1848
|
summaries = /* @__PURE__ */ new Map();
|
|
1849
|
+
activeTools = /* @__PURE__ */ new Map();
|
|
1818
1850
|
approvals = /* @__PURE__ */ new Map();
|
|
1819
1851
|
questions = /* @__PURE__ */ new Map();
|
|
1820
1852
|
archivedSessionIds = /* @__PURE__ */ new Set();
|
|
1853
|
+
/** Mirrors archived rows from the last sessions.list so the phone can browse
|
|
1854
|
+
* and restore them. Excluded from `summaries` and from the live broadcast. */
|
|
1855
|
+
archivedSummaries = /* @__PURE__ */ new Map();
|
|
1821
1856
|
subagentSessionIds = /* @__PURE__ */ new Set();
|
|
1822
1857
|
sinks = /* @__PURE__ */ new Set();
|
|
1823
1858
|
ring = [];
|
|
@@ -1852,6 +1887,7 @@ var HostBridge = class {
|
|
|
1852
1887
|
notifyAllCategories: true,
|
|
1853
1888
|
models: typeof this.apiProxy.sessions.models === "function" && typeof this.apiProxy.sessions.selectModel === "function",
|
|
1854
1889
|
sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
|
|
1890
|
+
sessionRestore: typeof this.apiProxy.workspace?.unarchiveSession === "function",
|
|
1855
1891
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
1856
1892
|
push: this.pushOutlet?.isAvailable() === true,
|
|
1857
1893
|
widgetPush: true,
|
|
@@ -1901,7 +1937,7 @@ var HostBridge = class {
|
|
|
1901
1937
|
* PROTOCOL §6 + §7), then fan the same payload out to offline devices
|
|
1902
1938
|
* holding an APNs token. */
|
|
1903
1939
|
emitNotify(args) {
|
|
1904
|
-
if (this.subagentSessionIds.has(args.sessionId)) return;
|
|
1940
|
+
if ((args.category === "turn.completed" || args.category === "session.error") && this.subagentSessionIds.has(args.sessionId)) return;
|
|
1905
1941
|
const body = args.body.length > 120 ? args.body.slice(0, 119) + "…" : args.body;
|
|
1906
1942
|
this.record("s2c.notify", {
|
|
1907
1943
|
notificationId: args.notificationId,
|
|
@@ -2077,9 +2113,12 @@ var HostBridge = class {
|
|
|
2077
2113
|
const event = frame.event;
|
|
2078
2114
|
const sessionId = String(frame.sessionId ?? "");
|
|
2079
2115
|
if (!event || !sessionId) break;
|
|
2116
|
+
if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
|
|
2117
|
+
if (this.subagentSessionIds.has(sessionId)) break;
|
|
2080
2118
|
this.noteActivity(sessionId, event);
|
|
2081
2119
|
this.captureAssistantText(sessionId, event);
|
|
2082
2120
|
const projection = projectEvent(sessionId, event);
|
|
2121
|
+
this.captureActivity(sessionId, event, projection?.data.tool);
|
|
2083
2122
|
if (projection) this.record("s2c.session.event", {
|
|
2084
2123
|
sessionId,
|
|
2085
2124
|
kind: projection.kind,
|
|
@@ -2114,6 +2153,7 @@ var HostBridge = class {
|
|
|
2114
2153
|
summary,
|
|
2115
2154
|
riskLevel: riskOf(toolName)
|
|
2116
2155
|
});
|
|
2156
|
+
if (p.callId) this.loadApprovalArguments(p.approvalId, p.callId);
|
|
2117
2157
|
this.emitNotify({
|
|
2118
2158
|
sessionId,
|
|
2119
2159
|
category: "approval.required",
|
|
@@ -2196,17 +2236,27 @@ var HostBridge = class {
|
|
|
2196
2236
|
const workspaceBySession = /* @__PURE__ */ new Map();
|
|
2197
2237
|
for (const workspace of workspaces) for (const sessionId of workspace.sessionIds ?? []) workspaceBySession.set(String(sessionId), workspace);
|
|
2198
2238
|
const next = /* @__PURE__ */ new Map();
|
|
2199
|
-
const
|
|
2239
|
+
const nextArchived = /* @__PURE__ */ new Map();
|
|
2200
2240
|
for (const row of response.result.value.items ?? []) {
|
|
2201
|
-
if (this.
|
|
2202
|
-
if (
|
|
2203
|
-
|
|
2241
|
+
if (isSubagentRow(row)) this.subagentSessionIds.add(row.sessionId);
|
|
2242
|
+
if (this.subagentSessionIds.has(row.sessionId)) continue;
|
|
2243
|
+
const summary = toSummary(row, this.approvals, this.questions, workspaceBySession.get(row.sessionId));
|
|
2244
|
+
if (this.archivedSessionIds.has(row.sessionId)) {
|
|
2245
|
+
nextArchived.set(row.sessionId, {
|
|
2246
|
+
...summary,
|
|
2247
|
+
archived: true
|
|
2248
|
+
});
|
|
2204
2249
|
continue;
|
|
2205
2250
|
}
|
|
2206
|
-
next.set(row.sessionId,
|
|
2251
|
+
next.set(row.sessionId, summary);
|
|
2252
|
+
}
|
|
2253
|
+
for (const [id, calls] of this.activeTools) {
|
|
2254
|
+
const row = next.get(id);
|
|
2255
|
+
if (!row || row.status !== "running" && !row.pendingApproval && !row.pendingQuestion) this.activeTools.delete(id);
|
|
2256
|
+
else row.activity = [...calls.values()].at(-1) ?? null;
|
|
2207
2257
|
}
|
|
2208
|
-
this.subagentSessionIds = subagentIds;
|
|
2209
2258
|
this.summaries = next;
|
|
2259
|
+
this.archivedSummaries = nextArchived;
|
|
2210
2260
|
const removedIds = [...previousIds].filter((id) => !next.has(id));
|
|
2211
2261
|
for (const id of this.lastAssistantText.keys()) if (!next.has(id)) this.lastAssistantText.delete(id);
|
|
2212
2262
|
this.record("s2c.sessions.delta", {
|
|
@@ -2224,6 +2274,24 @@ var HostBridge = class {
|
|
|
2224
2274
|
row.title = firstUser.text.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
2225
2275
|
this.pushSummary(row);
|
|
2226
2276
|
}
|
|
2277
|
+
captureActivity(sessionId, event, tool) {
|
|
2278
|
+
const row = this.summaries.get(sessionId);
|
|
2279
|
+
if (!row) return;
|
|
2280
|
+
const data = event.data;
|
|
2281
|
+
if (event.type === "tool/call" && data?.callId) {
|
|
2282
|
+
const calls = this.activeTools.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
2283
|
+
const detail = [tool?.name, tool?.summary].filter(Boolean).join(": ");
|
|
2284
|
+
calls.set(data.callId, Array.from(detail).slice(0, 160).join(""));
|
|
2285
|
+
if (calls.size > 64) calls.delete(calls.keys().next().value);
|
|
2286
|
+
this.activeTools.set(sessionId, calls);
|
|
2287
|
+
} else if (event.type === "tool/result" && data?.callId) this.activeTools.get(sessionId)?.delete(data.callId);
|
|
2288
|
+
else if (event.type === "turn/start" || event.type === "turn/end") this.activeTools.delete(sessionId);
|
|
2289
|
+
else return;
|
|
2290
|
+
const activity = [...this.activeTools.get(sessionId)?.values() ?? []].at(-1) ?? null;
|
|
2291
|
+
if (row.activity === activity) return;
|
|
2292
|
+
row.activity = activity;
|
|
2293
|
+
this.pushSummary(row);
|
|
2294
|
+
}
|
|
2227
2295
|
noteActivity(sessionId, event) {
|
|
2228
2296
|
const row = this.summaries.get(sessionId);
|
|
2229
2297
|
if (!row) return;
|
|
@@ -2282,6 +2350,14 @@ var HostBridge = class {
|
|
|
2282
2350
|
return [...this.summaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
2283
2351
|
}
|
|
2284
2352
|
/**
|
|
2353
|
+
* Archived rows from the last sessions.list, newest first. Served only on
|
|
2354
|
+
* demand so the live session list and its broadcast keep their shape for
|
|
2355
|
+
* clients that predate `c2s.sessions.archived`.
|
|
2356
|
+
*/
|
|
2357
|
+
listArchivedSessions() {
|
|
2358
|
+
return [...this.archivedSummaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2285
2361
|
* Complete transient interaction state. Unlike the replay ring, this remains
|
|
2286
2362
|
* authoritative after a long disconnect and is rehydrated by apiProxy's mux
|
|
2287
2363
|
* stream when the bridge itself restarts.
|
|
@@ -2293,6 +2369,7 @@ var HostBridge = class {
|
|
|
2293
2369
|
sessionId: pending.sessionId,
|
|
2294
2370
|
toolName: pending.toolName,
|
|
2295
2371
|
summary: pending.reason,
|
|
2372
|
+
...pending.toolArguments !== void 0 ? { toolArguments: pending.toolArguments } : {},
|
|
2296
2373
|
riskLevel: riskOf(pending.toolName)
|
|
2297
2374
|
})),
|
|
2298
2375
|
questions: [...this.questions.entries()].map(([requestId, pending]) => ({
|
|
@@ -2302,6 +2379,35 @@ var HostBridge = class {
|
|
|
2302
2379
|
}))
|
|
2303
2380
|
};
|
|
2304
2381
|
}
|
|
2382
|
+
/** Resolve only the exact invocation in this session; never guess from the latest tool. */
|
|
2383
|
+
async loadApprovalArguments(requestId, callId) {
|
|
2384
|
+
const pending = this.approvals.get(requestId);
|
|
2385
|
+
if (!pending) return;
|
|
2386
|
+
try {
|
|
2387
|
+
const response = await this.apiProxy.sessions.history({
|
|
2388
|
+
rpcId: randomUUID(),
|
|
2389
|
+
payload: {
|
|
2390
|
+
sessionId: pending.sessionId,
|
|
2391
|
+
maxMessages: 200
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
if (this.disposed || this.approvals.get(requestId) !== pending || !response.result?.ok) return;
|
|
2395
|
+
const args = ((response.result.value.events ?? []).map((row) => row.event).find((event) => {
|
|
2396
|
+
const data = event.data;
|
|
2397
|
+
return event.type === "tool/call" && data?.callId === callId && data?.name === pending.toolName;
|
|
2398
|
+
})?.data)?.arguments;
|
|
2399
|
+
if (typeof args !== "string" || !args.trim()) return;
|
|
2400
|
+
pending.toolArguments = args;
|
|
2401
|
+
this.record("s2c.pending.approval", {
|
|
2402
|
+
requestId,
|
|
2403
|
+
sessionId: pending.sessionId,
|
|
2404
|
+
toolName: pending.toolName,
|
|
2405
|
+
summary: pending.reason,
|
|
2406
|
+
riskLevel: riskOf(pending.toolName),
|
|
2407
|
+
toolArguments: args
|
|
2408
|
+
});
|
|
2409
|
+
} catch {}
|
|
2410
|
+
}
|
|
2305
2411
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
2306
2412
|
async openSession(sink, sessionId, tailCount) {
|
|
2307
2413
|
try {
|
|
@@ -2515,6 +2621,38 @@ var HostBridge = class {
|
|
|
2515
2621
|
};
|
|
2516
2622
|
}
|
|
2517
2623
|
}
|
|
2624
|
+
async unarchiveSession(sessionId) {
|
|
2625
|
+
const unarchive = this.apiProxy.workspace?.unarchiveSession;
|
|
2626
|
+
if (typeof unarchive !== "function") return {
|
|
2627
|
+
ok: false,
|
|
2628
|
+
kind: "unsupported",
|
|
2629
|
+
message: "session restore unavailable on this host version"
|
|
2630
|
+
};
|
|
2631
|
+
try {
|
|
2632
|
+
const response = await unarchive.call(this.apiProxy.workspace, {
|
|
2633
|
+
rpcId: randomUUID(),
|
|
2634
|
+
payload: { sessionId }
|
|
2635
|
+
});
|
|
2636
|
+
if (!response.result) return {
|
|
2637
|
+
ok: false,
|
|
2638
|
+
kind: "internal",
|
|
2639
|
+
message: "session restore returned no result"
|
|
2640
|
+
};
|
|
2641
|
+
if (!response.result.ok) return hostSessionManagementError(response.result.error);
|
|
2642
|
+
this.archivedSessionIds = new Set((response.result.value.archivedSessionIds ?? []).map(String));
|
|
2643
|
+
await this.refreshSummaries();
|
|
2644
|
+
return {
|
|
2645
|
+
ok: true,
|
|
2646
|
+
value: true
|
|
2647
|
+
};
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
return {
|
|
2650
|
+
ok: false,
|
|
2651
|
+
kind: "internal",
|
|
2652
|
+
message: String(error)
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2518
2656
|
async cancelSession(sessionId) {
|
|
2519
2657
|
const cancel = this.apiProxy.sessions.cancel;
|
|
2520
2658
|
if (typeof cancel !== "function") return {
|
|
@@ -3248,6 +3386,7 @@ var Dsh012ApiProxy = class {
|
|
|
3248
3386
|
if (session === void 0) throw new Error("dsh 0.1.2 sessionController is unavailable");
|
|
3249
3387
|
this.session = session;
|
|
3250
3388
|
this.workspaceController = ctx.get("workspaceController");
|
|
3389
|
+
if (typeof this.workspaceController?.unarchiveSession !== "function") delete this.workspace?.unarchiveSession;
|
|
3251
3390
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
3252
3391
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
3253
3392
|
}
|
|
@@ -3265,7 +3404,7 @@ var Dsh012ApiProxy = class {
|
|
|
3265
3404
|
}
|
|
3266
3405
|
sessions = {
|
|
3267
3406
|
list: async () => this.call(async () => {
|
|
3268
|
-
return { items: (await this.session.list({}, new AbortController().signal)).items.map(toPhoneSessionRow)
|
|
3407
|
+
return { items: (await this.session.list({}, new AbortController().signal)).items.map(toPhoneSessionRow) };
|
|
3269
3408
|
}),
|
|
3270
3409
|
history: async (request) => this.call(async () => {
|
|
3271
3410
|
const sessionId = request.payload.sessionId;
|
|
@@ -3327,6 +3466,12 @@ var Dsh012ApiProxy = class {
|
|
|
3327
3466
|
archiveSession: async (request) => this.call(async () => {
|
|
3328
3467
|
if (this.workspaceController === void 0) throw unavailable("workspace controller unavailable");
|
|
3329
3468
|
return { archivedSessionIds: [...(await this.workspaceController.archiveSession(request.payload)).archivedSessionIds] };
|
|
3469
|
+
}),
|
|
3470
|
+
unarchiveSession: async (request) => this.call(async () => {
|
|
3471
|
+
if (this.workspaceController === void 0) throw unavailable("workspace controller unavailable");
|
|
3472
|
+
const unarchive = this.workspaceController.unarchiveSession;
|
|
3473
|
+
if (typeof unarchive !== "function") throw unavailable("session restore unavailable on this host version");
|
|
3474
|
+
return { archivedSessionIds: [...(await unarchive.call(this.workspaceController, request.payload)).archivedSessionIds] };
|
|
3330
3475
|
})
|
|
3331
3476
|
};
|
|
3332
3477
|
host = {
|
|
@@ -3363,6 +3508,7 @@ var Dsh012ApiProxy = class {
|
|
|
3363
3508
|
queue.push({
|
|
3364
3509
|
type: "session/event",
|
|
3365
3510
|
sessionId: String(session.id),
|
|
3511
|
+
isSubagent: session.header?.origin === "subagent",
|
|
3366
3512
|
event
|
|
3367
3513
|
});
|
|
3368
3514
|
}), { global: true });
|
|
@@ -3411,7 +3557,8 @@ var Dsh012ApiProxy = class {
|
|
|
3411
3557
|
sessionId,
|
|
3412
3558
|
approvalId: rpcId,
|
|
3413
3559
|
toolName: String(request.toolName ?? "tool"),
|
|
3414
|
-
reason: String(request.reason ?? "")
|
|
3560
|
+
reason: String(request.reason ?? ""),
|
|
3561
|
+
...request.callId ? { callId: request.callId } : {}
|
|
3415
3562
|
});
|
|
3416
3563
|
return response.promise.finally(() => {
|
|
3417
3564
|
request.signal?.removeEventListener("abort", abort);
|
|
@@ -3750,10 +3897,16 @@ function parseLocal(value) {
|
|
|
3750
3897
|
if (!Array.isArray(endpoints) || endpoints.some((entry) => typeof entry !== "string")) reject("local.endpoints");
|
|
3751
3898
|
const message = s.message;
|
|
3752
3899
|
if (message !== void 0 && typeof message !== "string") reject("local.message");
|
|
3900
|
+
const tlsFingerprint = s.tlsFingerprint;
|
|
3901
|
+
if (tlsFingerprint !== void 0 && typeof tlsFingerprint !== "string") reject("local.tlsFingerprint");
|
|
3902
|
+
const tlsIdentityRegenerated = s.tlsIdentityRegenerated;
|
|
3903
|
+
if (tlsIdentityRegenerated !== void 0 && typeof tlsIdentityRegenerated !== "boolean") reject("local.tlsIdentityRegenerated");
|
|
3753
3904
|
return {
|
|
3754
3905
|
phase,
|
|
3755
3906
|
port: int(s, "port", "local.port"),
|
|
3756
3907
|
endpoints,
|
|
3908
|
+
...typeof tlsFingerprint === "string" ? { tlsFingerprint } : {},
|
|
3909
|
+
...tlsIdentityRegenerated === true ? { tlsIdentityRegenerated: true } : {},
|
|
3757
3910
|
...typeof message === "string" ? { message } : {},
|
|
3758
3911
|
updatedAt: int(s, "updatedAt", "local.updatedAt")
|
|
3759
3912
|
};
|
|
@@ -3865,6 +4018,35 @@ const scopesSchema = { parse(value) {
|
|
|
3865
4018
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
3866
4019
|
return normalizeDeviceScopes(value);
|
|
3867
4020
|
} };
|
|
4021
|
+
const booleanSchema = { parse(value) {
|
|
4022
|
+
if (typeof value !== "boolean") reject("boolean");
|
|
4023
|
+
return value;
|
|
4024
|
+
} };
|
|
4025
|
+
/**
|
|
4026
|
+
* One strict boundary codec carrying both generation shapes of the supported
|
|
4027
|
+
* `0.1.x` host family.
|
|
4028
|
+
*
|
|
4029
|
+
* Through `0.1.6-alpha.1` a descriptor published the schema value itself and
|
|
4030
|
+
* every consumer parsed through `codec.schema.parse(value)`. `0.1.6-alpha.2`
|
|
4031
|
+
* switched to a lazily materialized process-realm factory: `TypertRegistry`
|
|
4032
|
+
* now rejects a strict codec without `create()`, and the Gateway parses
|
|
4033
|
+
* through `codec.create().parse(value)`.
|
|
4034
|
+
*
|
|
4035
|
+
* Publishing both keys — `create()` returning the same hand-written,
|
|
4036
|
+
* dependency-free codec — keeps one package valid across the entire declared
|
|
4037
|
+
* peer range instead of forking the wire contract per host generation. The
|
|
4038
|
+
* cast is required because each generation's `TypertCodec` declares only its
|
|
4039
|
+
* own key, so a fresh object literal carrying both fails excess-property
|
|
4040
|
+
* checking against either one.
|
|
4041
|
+
*/
|
|
4042
|
+
function strictCodec(typeSymbol, schema) {
|
|
4043
|
+
return {
|
|
4044
|
+
mode: "strict",
|
|
4045
|
+
typeSymbol,
|
|
4046
|
+
schema,
|
|
4047
|
+
create: () => schema
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
3868
4050
|
const REPORT_HOST_CONTRIBUTION = {
|
|
3869
4051
|
package: REPORT_REMOTE_PACKAGE,
|
|
3870
4052
|
face: "host",
|
|
@@ -3882,11 +4064,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
3882
4064
|
method: "report",
|
|
3883
4065
|
invocation: { kind: "direct" },
|
|
3884
4066
|
parameters: [],
|
|
3885
|
-
result: {
|
|
3886
|
-
mode: "strict",
|
|
3887
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeepPilotReport`,
|
|
3888
|
-
schema: reportSchema
|
|
3889
|
-
}
|
|
4067
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeepPilotReport`, reportSchema)
|
|
3890
4068
|
},
|
|
3891
4069
|
{
|
|
3892
4070
|
id: `${REPORT_REMOTE_PACKAGE}#${BEGIN_PAIRING_ENDPOINT}`,
|
|
@@ -3895,11 +4073,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
3895
4073
|
method: "beginPairing",
|
|
3896
4074
|
invocation: { kind: "direct" },
|
|
3897
4075
|
parameters: [],
|
|
3898
|
-
result: {
|
|
3899
|
-
mode: "strict",
|
|
3900
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`,
|
|
3901
|
-
schema: pairingGrantSchema
|
|
3902
|
-
}
|
|
4076
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`, pairingGrantSchema)
|
|
3903
4077
|
},
|
|
3904
4078
|
{
|
|
3905
4079
|
id: `${REPORT_REMOTE_PACKAGE}#${REVOKE_DEVICE_ENDPOINT}`,
|
|
@@ -3911,20 +4085,9 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
3911
4085
|
name: "deviceId",
|
|
3912
4086
|
wire: "deviceId",
|
|
3913
4087
|
source: "json",
|
|
3914
|
-
codec: {
|
|
3915
|
-
mode: "strict",
|
|
3916
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
3917
|
-
schema: deviceIdSchema
|
|
3918
|
-
}
|
|
4088
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
3919
4089
|
}],
|
|
3920
|
-
result: {
|
|
3921
|
-
mode: "strict",
|
|
3922
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#Boolean`,
|
|
3923
|
-
schema: { parse(value) {
|
|
3924
|
-
if (typeof value !== "boolean") reject("boolean");
|
|
3925
|
-
return value;
|
|
3926
|
-
} }
|
|
3927
|
-
}
|
|
4090
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
3928
4091
|
},
|
|
3929
4092
|
{
|
|
3930
4093
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
@@ -3936,26 +4099,14 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
3936
4099
|
name: "deviceId",
|
|
3937
4100
|
wire: "deviceId",
|
|
3938
4101
|
source: "json",
|
|
3939
|
-
codec: {
|
|
3940
|
-
mode: "strict",
|
|
3941
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
3942
|
-
schema: deviceIdSchema
|
|
3943
|
-
}
|
|
4102
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
3944
4103
|
}, {
|
|
3945
4104
|
name: "scopes",
|
|
3946
4105
|
wire: "scopes",
|
|
3947
4106
|
source: "json",
|
|
3948
|
-
codec: {
|
|
3949
|
-
mode: "strict",
|
|
3950
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
3951
|
-
schema: scopesSchema
|
|
3952
|
-
}
|
|
4107
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
3953
4108
|
}],
|
|
3954
|
-
result: {
|
|
3955
|
-
mode: "strict",
|
|
3956
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
3957
|
-
schema: scopesSchema
|
|
3958
|
-
}
|
|
4109
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
3959
4110
|
},
|
|
3960
4111
|
{
|
|
3961
4112
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testRelay`,
|
|
@@ -3964,11 +4115,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
3964
4115
|
method: "testRelay",
|
|
3965
4116
|
invocation: { kind: "direct" },
|
|
3966
4117
|
parameters: [],
|
|
3967
|
-
result: {
|
|
3968
|
-
mode: "strict",
|
|
3969
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#RelayTestResult`,
|
|
3970
|
-
schema: relayTestSchema
|
|
3971
|
-
}
|
|
4118
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#RelayTestResult`, relayTestSchema)
|
|
3972
4119
|
},
|
|
3973
4120
|
{
|
|
3974
4121
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testPush`,
|
|
@@ -3977,11 +4124,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
3977
4124
|
method: "testPush",
|
|
3978
4125
|
invocation: { kind: "direct" },
|
|
3979
4126
|
parameters: [],
|
|
3980
|
-
result: {
|
|
3981
|
-
mode: "strict",
|
|
3982
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PushTestResult`,
|
|
3983
|
-
schema: pushTestSchema
|
|
3984
|
-
}
|
|
4127
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PushTestResult`, pushTestSchema)
|
|
3985
4128
|
}
|
|
3986
4129
|
]
|
|
3987
4130
|
};
|
|
@@ -5120,9 +5263,10 @@ const MAX_LOCAL_PORT = 65535;
|
|
|
5120
5263
|
function normalizeLocalPort(value) {
|
|
5121
5264
|
return typeof value === "number" && Number.isInteger(value) && value >= 1024 && value <= 65535 ? value : DEFAULT_LOCAL_PORT;
|
|
5122
5265
|
}
|
|
5266
|
+
/** The LAN listener is TLS-only, so every advertised endpoint is `https://`. */
|
|
5123
5267
|
function localEndpointURLs(addresses, port) {
|
|
5124
5268
|
const normalizedPort = normalizeLocalPort(port);
|
|
5125
|
-
return [...new Set(addresses)].map((address) => `
|
|
5269
|
+
return [...new Set(addresses)].map((address) => `https://${address}:${normalizedPort}`);
|
|
5126
5270
|
}
|
|
5127
5271
|
function localListenError(error, port) {
|
|
5128
5272
|
const code = error?.code;
|
|
@@ -5141,7 +5285,7 @@ const Config = z.object({
|
|
|
5141
5285
|
debug: z.boolean().default(false),
|
|
5142
5286
|
local: z.object({
|
|
5143
5287
|
enabled: z.boolean().default(true),
|
|
5144
|
-
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot
|
|
5288
|
+
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
|
|
5145
5289
|
}).default({
|
|
5146
5290
|
enabled: true,
|
|
5147
5291
|
port: DEFAULT_LOCAL_PORT
|
|
@@ -5430,15 +5574,15 @@ var WidgetPushScheduler = class {
|
|
|
5430
5574
|
//#endregion
|
|
5431
5575
|
//#region src/live-activity.ts
|
|
5432
5576
|
function liveActivityState(session) {
|
|
5433
|
-
const total = Math.max(0, session?.todos?.total ?? 0);
|
|
5577
|
+
const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
|
|
5434
5578
|
const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
|
|
5435
5579
|
const task = session?.todoItems?.find((item) => item.status === "in_progress") ?? session?.todoItems?.find((item) => item.status === "pending");
|
|
5436
5580
|
return {
|
|
5437
5581
|
title: Array.from(session?.title ?? "").slice(0, 100).join(""),
|
|
5438
|
-
task: Array.from(task?.content
|
|
5582
|
+
task: Array.from(session?.activity?.trim() || task?.content || "").slice(0, 160).join(""),
|
|
5439
5583
|
done,
|
|
5440
5584
|
total,
|
|
5441
|
-
phase: !session ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
|
|
5585
|
+
phase: !session || total === 0 ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
|
|
5442
5586
|
};
|
|
5443
5587
|
}
|
|
5444
5588
|
/** Persist terminal state before coalescing so a rapid next round cannot revive an activity. */
|
|
@@ -5506,9 +5650,13 @@ var LiveActivityPushManager = class {
|
|
|
5506
5650
|
* A deliberately narrow transport listener. Both the LAN endpoint and the
|
|
5507
5651
|
* loopback-only Funnel origin use this factory, so neither can accidentally
|
|
5508
5652
|
* inherit DSH's wider web/API route surface.
|
|
5653
|
+
*
|
|
5654
|
+
* With `tls` the listener speaks HTTPS/WSS only (the LAN posture); without
|
|
5655
|
+
* it the listener is plain HTTP, which is reserved for the loopback Funnel
|
|
5656
|
+
* origin where tailscaled terminates TLS.
|
|
5509
5657
|
*/
|
|
5510
|
-
function createPhoneServer(handlers) {
|
|
5511
|
-
const
|
|
5658
|
+
function createPhoneServer(handlers, tls) {
|
|
5659
|
+
const onRequest = (req, res) => {
|
|
5512
5660
|
const path = requestPath(req);
|
|
5513
5661
|
if (path === "/phone/health") handlers.health(req, res);
|
|
5514
5662
|
else if (path === "/phone/pair") handlers.pair(req, res);
|
|
@@ -5516,7 +5664,12 @@ function createPhoneServer(handlers) {
|
|
|
5516
5664
|
res.statusCode = 404;
|
|
5517
5665
|
res.end("not found");
|
|
5518
5666
|
}
|
|
5519
|
-
}
|
|
5667
|
+
};
|
|
5668
|
+
const server = tls === void 0 ? createServer$1(onRequest) : createServer({
|
|
5669
|
+
key: tls.key,
|
|
5670
|
+
cert: tls.cert,
|
|
5671
|
+
minVersion: "TLSv1.2"
|
|
5672
|
+
}, onRequest);
|
|
5520
5673
|
server.on("upgrade", (req, socket, head) => {
|
|
5521
5674
|
if (requestPath(req) !== "/phone") {
|
|
5522
5675
|
socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
@@ -5553,12 +5706,175 @@ function closeServer(server) {
|
|
|
5553
5706
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
5554
5707
|
}
|
|
5555
5708
|
//#endregion
|
|
5709
|
+
//#region src/lan-tls.ts
|
|
5710
|
+
const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
|
|
5711
|
+
const LAN_TLS_KEY_FILE = "key.pem";
|
|
5712
|
+
const LAN_TLS_CERT_FILE = "cert.pem";
|
|
5713
|
+
const CERT_VALIDITY_MS = 31536e7;
|
|
5714
|
+
/** Re-sign this far ahead of expiry so a long-running host never serves a stale leaf. */
|
|
5715
|
+
const CERT_RENEW_LEAD_MS = 2592e6;
|
|
5716
|
+
/** Compute the pinned public-key fingerprint of a PEM certificate. */
|
|
5717
|
+
function spkiFingerprint(certPem) {
|
|
5718
|
+
const der = createPublicKey(certPem).export({
|
|
5719
|
+
type: "spki",
|
|
5720
|
+
format: "der"
|
|
5721
|
+
});
|
|
5722
|
+
return "sha256:" + createHash("sha256").update(der).digest("base64url");
|
|
5723
|
+
}
|
|
5724
|
+
/**
|
|
5725
|
+
* Load the LAN listener's TLS identity from `dir`, creating or repairing it
|
|
5726
|
+
* as needed. The private key is the durable identity: it is created once and
|
|
5727
|
+
* reused for every subsequent certificate, so pinned devices survive
|
|
5728
|
+
* certificate renewals. Only a missing or unreadable key forces a new one.
|
|
5729
|
+
*/
|
|
5730
|
+
async function loadOrCreateLanTlsIdentity(dir, now = Date.now()) {
|
|
5731
|
+
await mkdir(dir, {
|
|
5732
|
+
recursive: true,
|
|
5733
|
+
mode: 448
|
|
5734
|
+
});
|
|
5735
|
+
const keyPath = join(dir, LAN_TLS_KEY_FILE);
|
|
5736
|
+
const certPath = join(dir, LAN_TLS_CERT_FILE);
|
|
5737
|
+
let key = await readPem(keyPath);
|
|
5738
|
+
let regenerated = false;
|
|
5739
|
+
if (key === void 0 || !validPrivateKey(key)) {
|
|
5740
|
+
regenerated = true;
|
|
5741
|
+
key = void 0;
|
|
5742
|
+
}
|
|
5743
|
+
let cert = regenerated ? void 0 : await readPem(certPath);
|
|
5744
|
+
let resigned = false;
|
|
5745
|
+
const existing = key !== void 0 && cert !== void 0 ? inspectCertificate(cert, key, now) : void 0;
|
|
5746
|
+
if (existing === void 0) {
|
|
5747
|
+
const issued = await issueCertificate(key, now);
|
|
5748
|
+
if (key === void 0) {
|
|
5749
|
+
key = pemFile(issued.key);
|
|
5750
|
+
await writePrivate(keyPath, key);
|
|
5751
|
+
} else resigned = true;
|
|
5752
|
+
cert = pemFile(issued.cert);
|
|
5753
|
+
await writePrivate(certPath, cert);
|
|
5754
|
+
}
|
|
5755
|
+
if (key === void 0 || cert === void 0) throw new Error("LAN TLS identity unavailable");
|
|
5756
|
+
return {
|
|
5757
|
+
key,
|
|
5758
|
+
cert,
|
|
5759
|
+
fingerprint: spkiFingerprint(cert),
|
|
5760
|
+
notAfter: existing ?? new X509Certificate(cert).validToDate.getTime(),
|
|
5761
|
+
regenerated,
|
|
5762
|
+
resigned
|
|
5763
|
+
};
|
|
5764
|
+
}
|
|
5765
|
+
async function readPem(path) {
|
|
5766
|
+
try {
|
|
5767
|
+
const text = await readFile(path, "utf8");
|
|
5768
|
+
return text.trim().length > 0 ? text : void 0;
|
|
5769
|
+
} catch (error) {
|
|
5770
|
+
if (error.code === "ENOENT") return void 0;
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
function validPrivateKey(pem) {
|
|
5775
|
+
try {
|
|
5776
|
+
return createPrivateKey(pem).asymmetricKeyType === "ec";
|
|
5777
|
+
} catch {
|
|
5778
|
+
return false;
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
/**
|
|
5782
|
+
* Return the certificate's expiry when it is usable with `keyPem`, or
|
|
5783
|
+
* `undefined` when it must be re-issued (malformed, wrong key, or expiring).
|
|
5784
|
+
*/
|
|
5785
|
+
function inspectCertificate(certPem, keyPem, now) {
|
|
5786
|
+
try {
|
|
5787
|
+
const cert = new X509Certificate(certPem);
|
|
5788
|
+
const certSpki = cert.publicKey.export({
|
|
5789
|
+
type: "spki",
|
|
5790
|
+
format: "der"
|
|
5791
|
+
});
|
|
5792
|
+
const keySpki = createPublicKey(createPrivateKey(keyPem)).export({
|
|
5793
|
+
type: "spki",
|
|
5794
|
+
format: "der"
|
|
5795
|
+
});
|
|
5796
|
+
if (!certSpki.equals(keySpki)) return void 0;
|
|
5797
|
+
const notAfter = cert.validToDate.getTime();
|
|
5798
|
+
if (!(cert.validFromDate.getTime() <= now && notAfter - CERT_RENEW_LEAD_MS > now)) return void 0;
|
|
5799
|
+
return notAfter;
|
|
5800
|
+
} catch {
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
async function issueCertificate(existingKey, now) {
|
|
5805
|
+
const notBeforeDate = /* @__PURE__ */ new Date(now - 3e5);
|
|
5806
|
+
const notAfterDate = new Date(now + CERT_VALIDITY_MS);
|
|
5807
|
+
const keyPair = existingKey === void 0 ? void 0 : {
|
|
5808
|
+
privateKey: createPrivateKey(existingKey).export({
|
|
5809
|
+
type: "pkcs8",
|
|
5810
|
+
format: "pem"
|
|
5811
|
+
}),
|
|
5812
|
+
publicKey: createPublicKey(existingKey).export({
|
|
5813
|
+
type: "spki",
|
|
5814
|
+
format: "pem"
|
|
5815
|
+
})
|
|
5816
|
+
};
|
|
5817
|
+
const result = await generate([{
|
|
5818
|
+
name: "commonName",
|
|
5819
|
+
value: LAN_TLS_COMMON_NAME
|
|
5820
|
+
}], {
|
|
5821
|
+
keyType: "ec",
|
|
5822
|
+
curve: "P-256",
|
|
5823
|
+
algorithm: "sha256",
|
|
5824
|
+
notBeforeDate,
|
|
5825
|
+
notAfterDate,
|
|
5826
|
+
...keyPair ? { keyPair } : {},
|
|
5827
|
+
extensions: [
|
|
5828
|
+
{
|
|
5829
|
+
name: "basicConstraints",
|
|
5830
|
+
cA: false,
|
|
5831
|
+
critical: true
|
|
5832
|
+
},
|
|
5833
|
+
{
|
|
5834
|
+
name: "keyUsage",
|
|
5835
|
+
digitalSignature: true,
|
|
5836
|
+
keyAgreement: true,
|
|
5837
|
+
critical: true
|
|
5838
|
+
},
|
|
5839
|
+
{
|
|
5840
|
+
name: "extKeyUsage",
|
|
5841
|
+
serverAuth: true
|
|
5842
|
+
},
|
|
5843
|
+
{
|
|
5844
|
+
name: "subjectAltName",
|
|
5845
|
+
altNames: [{
|
|
5846
|
+
type: 2,
|
|
5847
|
+
value: LAN_TLS_COMMON_NAME
|
|
5848
|
+
}]
|
|
5849
|
+
}
|
|
5850
|
+
]
|
|
5851
|
+
});
|
|
5852
|
+
return {
|
|
5853
|
+
key: existingKey ?? result.private,
|
|
5854
|
+
cert: result.cert
|
|
5855
|
+
};
|
|
5856
|
+
}
|
|
5857
|
+
/** Exact on-disk form, so a freshly issued value equals its later reload. */
|
|
5858
|
+
function pemFile(pem) {
|
|
5859
|
+
return pem.replace(/\r\n/g, "\n").trimEnd() + "\n";
|
|
5860
|
+
}
|
|
5861
|
+
async function writePrivate(path, contents) {
|
|
5862
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
5863
|
+
try {
|
|
5864
|
+
await writeFile(tmp, contents, { mode: 384 });
|
|
5865
|
+
await rename(tmp, path);
|
|
5866
|
+
} catch (error) {
|
|
5867
|
+
await unlink(tmp).catch(() => {});
|
|
5868
|
+
throw error;
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
//#endregion
|
|
5556
5872
|
//#region src/index.ts
|
|
5557
5873
|
/**
|
|
5558
5874
|
* dsh-deeppilot — data bridge between the DSH host and DeepPilot
|
|
5559
|
-
* clients. Owns independent, narrowly routed LAN
|
|
5560
|
-
*
|
|
5561
|
-
*
|
|
5875
|
+
* clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
|
|
5876
|
+
* devices) and loopback Funnel-origin listeners. The web UI and the rest of
|
|
5877
|
+
* DSH's API are never exposed by these listeners.
|
|
5562
5878
|
*
|
|
5563
5879
|
* Data plane: an in-process HostBridge consumes a local compatibility façade
|
|
5564
5880
|
* over DSH 0.1.2 Session/Workspace controllers, mirrors session summaries,
|
|
@@ -5569,7 +5885,7 @@ function closeServer(server) {
|
|
|
5569
5885
|
* Swift models mirror that v2 contract.
|
|
5570
5886
|
*/
|
|
5571
5887
|
const name = "deeppilot";
|
|
5572
|
-
/** No
|
|
5888
|
+
/** No web-service requirement: the plugin owns its own transport listeners. */
|
|
5573
5889
|
const inject = [];
|
|
5574
5890
|
const SERVER_VERSION = readOwnPackageVersion();
|
|
5575
5891
|
const MAX_CLIENT_CONNECTIONS = 16;
|
|
@@ -6100,9 +6416,27 @@ function apply(ctx, options) {
|
|
|
6100
6416
|
endpoints: [],
|
|
6101
6417
|
updatedAt: Date.now()
|
|
6102
6418
|
};
|
|
6419
|
+
let lanTlsIdentity;
|
|
6420
|
+
let tlsIdentityRegenerated = false;
|
|
6421
|
+
const loadLanTls = () => {
|
|
6422
|
+
lanTlsIdentity ??= (async () => {
|
|
6423
|
+
await ready;
|
|
6424
|
+
const identity = await loadOrCreateLanTlsIdentity(join(dataDir, "lan-tls"));
|
|
6425
|
+
if (identity.regenerated) {
|
|
6426
|
+
tlsIdentityRegenerated = true;
|
|
6427
|
+
log(`LAN TLS identity created: fingerprint=${identity.fingerprint}; previously paired LAN devices must pair again`);
|
|
6428
|
+
} else if (identity.resigned) log(`LAN TLS certificate renewed (fingerprint unchanged: ${identity.fingerprint})`);
|
|
6429
|
+
return identity;
|
|
6430
|
+
})();
|
|
6431
|
+
lanTlsIdentity.catch(() => {
|
|
6432
|
+
lanTlsIdentity = void 0;
|
|
6433
|
+
});
|
|
6434
|
+
return lanTlsIdentity;
|
|
6435
|
+
};
|
|
6103
6436
|
const localStatus = (addresses) => ({
|
|
6104
6437
|
...localState,
|
|
6105
|
-
endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : []
|
|
6438
|
+
endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : [],
|
|
6439
|
+
...tlsIdentityRegenerated ? { tlsIdentityRegenerated: true } : {}
|
|
6106
6440
|
});
|
|
6107
6441
|
const remoteStatus = () => remoteSupervisor?.status() ?? {
|
|
6108
6442
|
provider: "tailscale-funnel",
|
|
@@ -6302,13 +6636,15 @@ function apply(ctx, options) {
|
|
|
6302
6636
|
}, Date.now());
|
|
6303
6637
|
authRateLimiter.recordSuccess(source);
|
|
6304
6638
|
log(`device paired id=${auditLabel(record.deviceId)} source=${auditLabel(source)}`);
|
|
6639
|
+
const tlsFingerprint = req.socket.encrypted === true ? (await lanTlsIdentity)?.fingerprint : void 0;
|
|
6305
6640
|
res.statusCode = 201;
|
|
6306
6641
|
res.end(JSON.stringify({
|
|
6307
6642
|
ok: true,
|
|
6308
6643
|
v: 2,
|
|
6309
6644
|
deviceId: record.deviceId,
|
|
6310
6645
|
audience,
|
|
6311
|
-
scopes: record.scopes ?? []
|
|
6646
|
+
scopes: record.scopes ?? [],
|
|
6647
|
+
...tlsFingerprint ? { tlsFingerprint } : {}
|
|
6312
6648
|
}));
|
|
6313
6649
|
} catch (error) {
|
|
6314
6650
|
res.statusCode = error instanceof SyntaxError || error instanceof TypeError ? 400 : 503;
|
|
@@ -6461,9 +6797,15 @@ function apply(ctx, options) {
|
|
|
6461
6797
|
endpoints: [],
|
|
6462
6798
|
updatedAt: Date.now()
|
|
6463
6799
|
};
|
|
6464
|
-
|
|
6465
|
-
localServer = server;
|
|
6800
|
+
let server;
|
|
6466
6801
|
try {
|
|
6802
|
+
const tls = await loadLanTls();
|
|
6803
|
+
if (localDisposed || appliedLocalKey !== nextKey) return;
|
|
6804
|
+
server = createPhoneServer(phoneHandlers, {
|
|
6805
|
+
key: tls.key,
|
|
6806
|
+
cert: tls.cert
|
|
6807
|
+
});
|
|
6808
|
+
localServer = server;
|
|
6467
6809
|
await listen(server, next.port, "0.0.0.0");
|
|
6468
6810
|
if (localDisposed || localServer !== server) {
|
|
6469
6811
|
await closeServer(server);
|
|
@@ -6473,12 +6815,15 @@ function apply(ctx, options) {
|
|
|
6473
6815
|
phase: "online",
|
|
6474
6816
|
port: next.port,
|
|
6475
6817
|
endpoints: [],
|
|
6818
|
+
tlsFingerprint: tls.fingerprint,
|
|
6476
6819
|
updatedAt: Date.now()
|
|
6477
6820
|
};
|
|
6478
|
-
log(`local transport listening on 0.0.0.0:${next.port}`);
|
|
6821
|
+
log(`local transport listening on https://0.0.0.0:${next.port} (tls fingerprint ${tls.fingerprint})`);
|
|
6479
6822
|
} catch (error) {
|
|
6480
|
-
if (
|
|
6481
|
-
|
|
6823
|
+
if (server !== void 0) {
|
|
6824
|
+
if (localServer === server) localServer = void 0;
|
|
6825
|
+
await closeServer(server);
|
|
6826
|
+
}
|
|
6482
6827
|
const message = localListenError(error, next.port);
|
|
6483
6828
|
localState = {
|
|
6484
6829
|
phase: "error",
|
|
@@ -6611,30 +6956,6 @@ function apply(ctx, options) {
|
|
|
6611
6956
|
bridge.dispose();
|
|
6612
6957
|
}, "deeppilot: host streams");
|
|
6613
6958
|
});
|
|
6614
|
-
ctx.inject(["webServer"], (sub) => {
|
|
6615
|
-
const webCtx = sub;
|
|
6616
|
-
const web = webCtx.webServer;
|
|
6617
|
-
if (!web) {
|
|
6618
|
-
log("webServer service absent; 3080 compatibility routes unavailable (independent transports remain active)");
|
|
6619
|
-
return;
|
|
6620
|
-
}
|
|
6621
|
-
webCtx.effect(() => web.registerUpgrade({
|
|
6622
|
-
path: "/phone",
|
|
6623
|
-
handler: handleUpgrade
|
|
6624
|
-
}), "deeppilot: /phone WebSocket");
|
|
6625
|
-
webCtx.effect(() => web.register({
|
|
6626
|
-
kind: "exact",
|
|
6627
|
-
path: "/phone/health",
|
|
6628
|
-
handler: handleHealth
|
|
6629
|
-
}), "deeppilot: /phone/health");
|
|
6630
|
-
webCtx.effect(() => web.register({
|
|
6631
|
-
kind: "exact",
|
|
6632
|
-
path: "/phone/pair",
|
|
6633
|
-
handler: handlePair
|
|
6634
|
-
}), "deeppilot: /phone/pair");
|
|
6635
|
-
if (enabledNow()) log("/phone compatibility routes registered on DSH web server");
|
|
6636
|
-
else log("bridge disabled; /phone refuses connections until re-enabled");
|
|
6637
|
-
});
|
|
6638
6959
|
ctx.effect(() => async () => {
|
|
6639
6960
|
closeAllConnections();
|
|
6640
6961
|
const bridge = state.bridge;
|