dsh-deeppilot 0.6.2 → 0.7.0

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/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: event.seq,
1441
- role: "tool",
1442
- ok: !event.data || data?.error === void 0,
1443
- ...data?.callId ? { callId: String(data.callId) } : {},
1444
- ts: tsOf(event)
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
- if (target?.tool) target.tool = {
1683
- ...target.tool,
1684
- state: failed ? "error" : "ok",
1685
- summary
1686
- };
1687
- else messages.push({
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 subagentIds = /* @__PURE__ */ new Set();
2239
+ const nextArchived = /* @__PURE__ */ new Map();
2200
2240
  for (const row of response.result.value.items ?? []) {
2201
- if (this.archivedSessionIds.has(row.sessionId)) continue;
2202
- if (isSubagentRow(row)) {
2203
- subagentIds.add(row.sessionId);
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, toSummary(row, this.approvals, this.questions, workspaceBySession.get(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).filter((row) => !isSubagentRow(row)) };
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
  };
@@ -5120,9 +5273,10 @@ const MAX_LOCAL_PORT = 65535;
5120
5273
  function normalizeLocalPort(value) {
5121
5274
  return typeof value === "number" && Number.isInteger(value) && value >= 1024 && value <= 65535 ? value : DEFAULT_LOCAL_PORT;
5122
5275
  }
5276
+ /** The LAN listener is TLS-only, so every advertised endpoint is `https://`. */
5123
5277
  function localEndpointURLs(addresses, port) {
5124
5278
  const normalizedPort = normalizeLocalPort(port);
5125
- return [...new Set(addresses)].map((address) => `http://${address}:${normalizedPort}`);
5279
+ return [...new Set(addresses)].map((address) => `https://${address}:${normalizedPort}`);
5126
5280
  }
5127
5281
  function localListenError(error, port) {
5128
5282
  const code = error?.code;
@@ -5141,7 +5295,7 @@ const Config = z.object({
5141
5295
  debug: z.boolean().default(false),
5142
5296
  local: z.object({
5143
5297
  enabled: z.boolean().default(true),
5144
- port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(默认 3098,修改后本地连接会短暂重连)")
5298
+ port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
5145
5299
  }).default({
5146
5300
  enabled: true,
5147
5301
  port: DEFAULT_LOCAL_PORT
@@ -5430,15 +5584,15 @@ var WidgetPushScheduler = class {
5430
5584
  //#endregion
5431
5585
  //#region src/live-activity.ts
5432
5586
  function liveActivityState(session) {
5433
- const total = Math.max(0, session?.todos?.total ?? 0);
5587
+ const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
5434
5588
  const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
5435
5589
  const task = session?.todoItems?.find((item) => item.status === "in_progress") ?? session?.todoItems?.find((item) => item.status === "pending");
5436
5590
  return {
5437
5591
  title: Array.from(session?.title ?? "").slice(0, 100).join(""),
5438
- task: Array.from(task?.content ?? "").slice(0, 160).join(""),
5592
+ task: Array.from(session?.activity?.trim() || task?.content || "").slice(0, 160).join(""),
5439
5593
  done,
5440
5594
  total,
5441
- phase: !session ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
5595
+ phase: !session || total === 0 ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
5442
5596
  };
5443
5597
  }
5444
5598
  /** Persist terminal state before coalescing so a rapid next round cannot revive an activity. */
@@ -5506,9 +5660,13 @@ var LiveActivityPushManager = class {
5506
5660
  * A deliberately narrow transport listener. Both the LAN endpoint and the
5507
5661
  * loopback-only Funnel origin use this factory, so neither can accidentally
5508
5662
  * inherit DSH's wider web/API route surface.
5663
+ *
5664
+ * With `tls` the listener speaks HTTPS/WSS only (the LAN posture); without
5665
+ * it the listener is plain HTTP, which is reserved for the loopback Funnel
5666
+ * origin where tailscaled terminates TLS.
5509
5667
  */
5510
- function createPhoneServer(handlers) {
5511
- const server = createServer((req, res) => {
5668
+ function createPhoneServer(handlers, tls) {
5669
+ const onRequest = (req, res) => {
5512
5670
  const path = requestPath(req);
5513
5671
  if (path === "/phone/health") handlers.health(req, res);
5514
5672
  else if (path === "/phone/pair") handlers.pair(req, res);
@@ -5516,7 +5674,12 @@ function createPhoneServer(handlers) {
5516
5674
  res.statusCode = 404;
5517
5675
  res.end("not found");
5518
5676
  }
5519
- });
5677
+ };
5678
+ const server = tls === void 0 ? createServer$1(onRequest) : createServer({
5679
+ key: tls.key,
5680
+ cert: tls.cert,
5681
+ minVersion: "TLSv1.2"
5682
+ }, onRequest);
5520
5683
  server.on("upgrade", (req, socket, head) => {
5521
5684
  if (requestPath(req) !== "/phone") {
5522
5685
  socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
@@ -5553,12 +5716,175 @@ function closeServer(server) {
5553
5716
  return new Promise((resolve) => server.close(() => resolve()));
5554
5717
  }
5555
5718
  //#endregion
5719
+ //#region src/lan-tls.ts
5720
+ const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
5721
+ const LAN_TLS_KEY_FILE = "key.pem";
5722
+ const LAN_TLS_CERT_FILE = "cert.pem";
5723
+ const CERT_VALIDITY_MS = 31536e7;
5724
+ /** Re-sign this far ahead of expiry so a long-running host never serves a stale leaf. */
5725
+ const CERT_RENEW_LEAD_MS = 2592e6;
5726
+ /** Compute the pinned public-key fingerprint of a PEM certificate. */
5727
+ function spkiFingerprint(certPem) {
5728
+ const der = createPublicKey(certPem).export({
5729
+ type: "spki",
5730
+ format: "der"
5731
+ });
5732
+ return "sha256:" + createHash("sha256").update(der).digest("base64url");
5733
+ }
5734
+ /**
5735
+ * Load the LAN listener's TLS identity from `dir`, creating or repairing it
5736
+ * as needed. The private key is the durable identity: it is created once and
5737
+ * reused for every subsequent certificate, so pinned devices survive
5738
+ * certificate renewals. Only a missing or unreadable key forces a new one.
5739
+ */
5740
+ async function loadOrCreateLanTlsIdentity(dir, now = Date.now()) {
5741
+ await mkdir(dir, {
5742
+ recursive: true,
5743
+ mode: 448
5744
+ });
5745
+ const keyPath = join(dir, LAN_TLS_KEY_FILE);
5746
+ const certPath = join(dir, LAN_TLS_CERT_FILE);
5747
+ let key = await readPem(keyPath);
5748
+ let regenerated = false;
5749
+ if (key === void 0 || !validPrivateKey(key)) {
5750
+ regenerated = true;
5751
+ key = void 0;
5752
+ }
5753
+ let cert = regenerated ? void 0 : await readPem(certPath);
5754
+ let resigned = false;
5755
+ const existing = key !== void 0 && cert !== void 0 ? inspectCertificate(cert, key, now) : void 0;
5756
+ if (existing === void 0) {
5757
+ const issued = await issueCertificate(key, now);
5758
+ if (key === void 0) {
5759
+ key = pemFile(issued.key);
5760
+ await writePrivate(keyPath, key);
5761
+ } else resigned = true;
5762
+ cert = pemFile(issued.cert);
5763
+ await writePrivate(certPath, cert);
5764
+ }
5765
+ if (key === void 0 || cert === void 0) throw new Error("LAN TLS identity unavailable");
5766
+ return {
5767
+ key,
5768
+ cert,
5769
+ fingerprint: spkiFingerprint(cert),
5770
+ notAfter: existing ?? new X509Certificate(cert).validToDate.getTime(),
5771
+ regenerated,
5772
+ resigned
5773
+ };
5774
+ }
5775
+ async function readPem(path) {
5776
+ try {
5777
+ const text = await readFile(path, "utf8");
5778
+ return text.trim().length > 0 ? text : void 0;
5779
+ } catch (error) {
5780
+ if (error.code === "ENOENT") return void 0;
5781
+ return;
5782
+ }
5783
+ }
5784
+ function validPrivateKey(pem) {
5785
+ try {
5786
+ return createPrivateKey(pem).asymmetricKeyType === "ec";
5787
+ } catch {
5788
+ return false;
5789
+ }
5790
+ }
5791
+ /**
5792
+ * Return the certificate's expiry when it is usable with `keyPem`, or
5793
+ * `undefined` when it must be re-issued (malformed, wrong key, or expiring).
5794
+ */
5795
+ function inspectCertificate(certPem, keyPem, now) {
5796
+ try {
5797
+ const cert = new X509Certificate(certPem);
5798
+ const certSpki = cert.publicKey.export({
5799
+ type: "spki",
5800
+ format: "der"
5801
+ });
5802
+ const keySpki = createPublicKey(createPrivateKey(keyPem)).export({
5803
+ type: "spki",
5804
+ format: "der"
5805
+ });
5806
+ if (!certSpki.equals(keySpki)) return void 0;
5807
+ const notAfter = cert.validToDate.getTime();
5808
+ if (!(cert.validFromDate.getTime() <= now && notAfter - CERT_RENEW_LEAD_MS > now)) return void 0;
5809
+ return notAfter;
5810
+ } catch {
5811
+ return;
5812
+ }
5813
+ }
5814
+ async function issueCertificate(existingKey, now) {
5815
+ const notBeforeDate = /* @__PURE__ */ new Date(now - 3e5);
5816
+ const notAfterDate = new Date(now + CERT_VALIDITY_MS);
5817
+ const keyPair = existingKey === void 0 ? void 0 : {
5818
+ privateKey: createPrivateKey(existingKey).export({
5819
+ type: "pkcs8",
5820
+ format: "pem"
5821
+ }),
5822
+ publicKey: createPublicKey(existingKey).export({
5823
+ type: "spki",
5824
+ format: "pem"
5825
+ })
5826
+ };
5827
+ const result = await generate([{
5828
+ name: "commonName",
5829
+ value: LAN_TLS_COMMON_NAME
5830
+ }], {
5831
+ keyType: "ec",
5832
+ curve: "P-256",
5833
+ algorithm: "sha256",
5834
+ notBeforeDate,
5835
+ notAfterDate,
5836
+ ...keyPair ? { keyPair } : {},
5837
+ extensions: [
5838
+ {
5839
+ name: "basicConstraints",
5840
+ cA: false,
5841
+ critical: true
5842
+ },
5843
+ {
5844
+ name: "keyUsage",
5845
+ digitalSignature: true,
5846
+ keyAgreement: true,
5847
+ critical: true
5848
+ },
5849
+ {
5850
+ name: "extKeyUsage",
5851
+ serverAuth: true
5852
+ },
5853
+ {
5854
+ name: "subjectAltName",
5855
+ altNames: [{
5856
+ type: 2,
5857
+ value: LAN_TLS_COMMON_NAME
5858
+ }]
5859
+ }
5860
+ ]
5861
+ });
5862
+ return {
5863
+ key: existingKey ?? result.private,
5864
+ cert: result.cert
5865
+ };
5866
+ }
5867
+ /** Exact on-disk form, so a freshly issued value equals its later reload. */
5868
+ function pemFile(pem) {
5869
+ return pem.replace(/\r\n/g, "\n").trimEnd() + "\n";
5870
+ }
5871
+ async function writePrivate(path, contents) {
5872
+ const tmp = `${path}.${process.pid}.tmp`;
5873
+ try {
5874
+ await writeFile(tmp, contents, { mode: 384 });
5875
+ await rename(tmp, path);
5876
+ } catch (error) {
5877
+ await unlink(tmp).catch(() => {});
5878
+ throw error;
5879
+ }
5880
+ }
5881
+ //#endregion
5556
5882
  //#region src/index.ts
5557
5883
  /**
5558
5884
  * dsh-deeppilot — data bridge between the DSH host and DeepPilot
5559
- * clients. Owns independent, narrowly routed LAN and Funnel-origin listeners;
5560
- * temporary compatibility routes remain on the existing DSH web server. The
5561
- * web UI and the rest of DSH's API are never exposed by these listeners.
5885
+ * clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
5886
+ * devices) and loopback Funnel-origin listeners. The web UI and the rest of
5887
+ * DSH's API are never exposed by these listeners.
5562
5888
  *
5563
5889
  * Data plane: an in-process HostBridge consumes a local compatibility façade
5564
5890
  * over DSH 0.1.2 Session/Workspace controllers, mirrors session summaries,
@@ -5569,7 +5895,7 @@ function closeServer(server) {
5569
5895
  * Swift models mirror that v2 contract.
5570
5896
  */
5571
5897
  const name = "deeppilot";
5572
- /** No eager web-service requirement: independent transports start on their own. */
5898
+ /** No web-service requirement: the plugin owns its own transport listeners. */
5573
5899
  const inject = [];
5574
5900
  const SERVER_VERSION = readOwnPackageVersion();
5575
5901
  const MAX_CLIENT_CONNECTIONS = 16;
@@ -6100,9 +6426,27 @@ function apply(ctx, options) {
6100
6426
  endpoints: [],
6101
6427
  updatedAt: Date.now()
6102
6428
  };
6429
+ let lanTlsIdentity;
6430
+ let tlsIdentityRegenerated = false;
6431
+ const loadLanTls = () => {
6432
+ lanTlsIdentity ??= (async () => {
6433
+ await ready;
6434
+ const identity = await loadOrCreateLanTlsIdentity(join(dataDir, "lan-tls"));
6435
+ if (identity.regenerated) {
6436
+ tlsIdentityRegenerated = true;
6437
+ log(`LAN TLS identity created: fingerprint=${identity.fingerprint}; previously paired LAN devices must pair again`);
6438
+ } else if (identity.resigned) log(`LAN TLS certificate renewed (fingerprint unchanged: ${identity.fingerprint})`);
6439
+ return identity;
6440
+ })();
6441
+ lanTlsIdentity.catch(() => {
6442
+ lanTlsIdentity = void 0;
6443
+ });
6444
+ return lanTlsIdentity;
6445
+ };
6103
6446
  const localStatus = (addresses) => ({
6104
6447
  ...localState,
6105
- endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : []
6448
+ endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : [],
6449
+ ...tlsIdentityRegenerated ? { tlsIdentityRegenerated: true } : {}
6106
6450
  });
6107
6451
  const remoteStatus = () => remoteSupervisor?.status() ?? {
6108
6452
  provider: "tailscale-funnel",
@@ -6302,13 +6646,15 @@ function apply(ctx, options) {
6302
6646
  }, Date.now());
6303
6647
  authRateLimiter.recordSuccess(source);
6304
6648
  log(`device paired id=${auditLabel(record.deviceId)} source=${auditLabel(source)}`);
6649
+ const tlsFingerprint = req.socket.encrypted === true ? (await lanTlsIdentity)?.fingerprint : void 0;
6305
6650
  res.statusCode = 201;
6306
6651
  res.end(JSON.stringify({
6307
6652
  ok: true,
6308
6653
  v: 2,
6309
6654
  deviceId: record.deviceId,
6310
6655
  audience,
6311
- scopes: record.scopes ?? []
6656
+ scopes: record.scopes ?? [],
6657
+ ...tlsFingerprint ? { tlsFingerprint } : {}
6312
6658
  }));
6313
6659
  } catch (error) {
6314
6660
  res.statusCode = error instanceof SyntaxError || error instanceof TypeError ? 400 : 503;
@@ -6461,9 +6807,15 @@ function apply(ctx, options) {
6461
6807
  endpoints: [],
6462
6808
  updatedAt: Date.now()
6463
6809
  };
6464
- const server = createPhoneServer(phoneHandlers);
6465
- localServer = server;
6810
+ let server;
6466
6811
  try {
6812
+ const tls = await loadLanTls();
6813
+ if (localDisposed || appliedLocalKey !== nextKey) return;
6814
+ server = createPhoneServer(phoneHandlers, {
6815
+ key: tls.key,
6816
+ cert: tls.cert
6817
+ });
6818
+ localServer = server;
6467
6819
  await listen(server, next.port, "0.0.0.0");
6468
6820
  if (localDisposed || localServer !== server) {
6469
6821
  await closeServer(server);
@@ -6473,12 +6825,15 @@ function apply(ctx, options) {
6473
6825
  phase: "online",
6474
6826
  port: next.port,
6475
6827
  endpoints: [],
6828
+ tlsFingerprint: tls.fingerprint,
6476
6829
  updatedAt: Date.now()
6477
6830
  };
6478
- log(`local transport listening on 0.0.0.0:${next.port}`);
6831
+ log(`local transport listening on https://0.0.0.0:${next.port} (tls fingerprint ${tls.fingerprint})`);
6479
6832
  } catch (error) {
6480
- if (localServer === server) localServer = void 0;
6481
- await closeServer(server);
6833
+ if (server !== void 0) {
6834
+ if (localServer === server) localServer = void 0;
6835
+ await closeServer(server);
6836
+ }
6482
6837
  const message = localListenError(error, next.port);
6483
6838
  localState = {
6484
6839
  phase: "error",
@@ -6611,30 +6966,6 @@ function apply(ctx, options) {
6611
6966
  bridge.dispose();
6612
6967
  }, "deeppilot: host streams");
6613
6968
  });
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
6969
  ctx.effect(() => async () => {
6639
6970
  closeAllConnections();
6640
6971
  const bridge = state.bridge;