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/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
  };
@@ -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) => `http://${address}:${normalizedPort}`);
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 局域网独立端口(默认 3098,修改后本地连接会短暂重连)")
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 ?? "").slice(0, 160).join(""),
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 server = createServer((req, res) => {
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 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.
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 eager web-service requirement: independent transports start on their own. */
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
- const server = createPhoneServer(phoneHandlers);
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 (localServer === server) localServer = void 0;
6481
- await closeServer(server);
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;