@researai/deepscientist 1.5.6 → 1.5.8

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.
Files changed (158) hide show
  1. package/README.md +32 -0
  2. package/bin/ds.js +274 -18
  3. package/docs/en/07_MEMORY_AND_MCP.md +40 -3
  4. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  5. package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
  6. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  7. package/install.sh +34 -0
  8. package/package.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/src/deepscientist/__init__.py +1 -1
  11. package/src/deepscientist/acp/envelope.py +1 -0
  12. package/src/deepscientist/artifact/metrics.py +813 -80
  13. package/src/deepscientist/artifact/schemas.py +1 -0
  14. package/src/deepscientist/artifact/service.py +1101 -99
  15. package/src/deepscientist/bash_exec/monitor.py +1 -1
  16. package/src/deepscientist/bash_exec/service.py +17 -9
  17. package/src/deepscientist/channels/qq.py +17 -0
  18. package/src/deepscientist/channels/relay.py +16 -0
  19. package/src/deepscientist/cli.py +1 -1
  20. package/src/deepscientist/config/models.py +12 -6
  21. package/src/deepscientist/config/service.py +75 -2
  22. package/src/deepscientist/connector_profiles.py +34 -6
  23. package/src/deepscientist/daemon/api/handlers.py +290 -15
  24. package/src/deepscientist/daemon/api/router.py +2 -0
  25. package/src/deepscientist/daemon/app.py +521 -23
  26. package/src/deepscientist/gitops/diff.py +6 -10
  27. package/src/deepscientist/mcp/server.py +188 -39
  28. package/src/deepscientist/prompts/builder.py +71 -22
  29. package/src/deepscientist/qq_profiles.py +19 -9
  30. package/src/deepscientist/quest/layout.py +1 -0
  31. package/src/deepscientist/quest/service.py +83 -34
  32. package/src/deepscientist/quest/stage_views.py +74 -29
  33. package/src/deepscientist/runners/codex.py +32 -14
  34. package/src/deepscientist/runners/runtime_overrides.py +46 -0
  35. package/src/deepscientist/skills/installer.py +7 -0
  36. package/src/prompts/connectors/qq.md +1 -1
  37. package/src/prompts/contracts/shared_interaction.md +14 -0
  38. package/src/prompts/system.md +134 -30
  39. package/src/skills/analysis-campaign/SKILL.md +34 -8
  40. package/src/skills/analysis-campaign/references/campaign-checklist-template.md +41 -0
  41. package/src/skills/analysis-campaign/references/campaign-plan-template.md +68 -0
  42. package/src/skills/baseline/SKILL.md +145 -32
  43. package/src/skills/baseline/references/baseline-checklist-template.md +57 -0
  44. package/src/skills/baseline/references/baseline-plan-template.md +105 -0
  45. package/src/skills/decision/SKILL.md +12 -8
  46. package/src/skills/experiment/SKILL.md +51 -9
  47. package/src/skills/experiment/references/main-experiment-checklist-template.md +52 -0
  48. package/src/skills/experiment/references/main-experiment-plan-template.md +79 -0
  49. package/src/skills/figure-polish/SKILL.md +1 -0
  50. package/src/skills/finalize/SKILL.md +3 -8
  51. package/src/skills/idea/SKILL.md +2 -8
  52. package/src/skills/intake-audit/SKILL.md +2 -8
  53. package/src/skills/rebuttal/SKILL.md +2 -8
  54. package/src/skills/review/SKILL.md +2 -8
  55. package/src/skills/scout/SKILL.md +2 -8
  56. package/src/skills/write/SKILL.md +52 -16
  57. package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
  58. package/src/skills/write/templates/README.md +408 -0
  59. package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
  60. package/src/skills/write/templates/aaai2026/README.md +534 -0
  61. package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  62. package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  63. package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
  64. package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
  65. package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
  66. package/src/skills/write/templates/acl/README.md +50 -0
  67. package/src/skills/write/templates/acl/acl.sty +312 -0
  68. package/src/skills/write/templates/acl/acl_latex.tex +377 -0
  69. package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
  70. package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
  71. package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
  72. package/src/skills/write/templates/acl/custom.bib +70 -0
  73. package/src/skills/write/templates/acl/formatting.md +326 -0
  74. package/src/skills/write/templates/asplos2027/main.tex +459 -0
  75. package/src/skills/write/templates/asplos2027/references.bib +135 -0
  76. package/src/skills/write/templates/colm2025/README.md +3 -0
  77. package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
  78. package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
  79. package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
  80. package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
  81. package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
  82. package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
  83. package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
  84. package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
  85. package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
  86. package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
  87. package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
  88. package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
  89. package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
  90. package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
  91. package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
  92. package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
  93. package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
  94. package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
  95. package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
  96. package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
  97. package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
  98. package/src/skills/write/templates/neurips2025/Makefile +36 -0
  99. package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
  100. package/src/skills/write/templates/neurips2025/main.tex +38 -0
  101. package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
  102. package/src/skills/write/templates/nsdi2027/main.tex +426 -0
  103. package/src/skills/write/templates/nsdi2027/references.bib +151 -0
  104. package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
  105. package/src/skills/write/templates/osdi2026/main.tex +429 -0
  106. package/src/skills/write/templates/osdi2026/references.bib +150 -0
  107. package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
  108. package/src/skills/write/templates/sosp2026/main.tex +532 -0
  109. package/src/skills/write/templates/sosp2026/references.bib +148 -0
  110. package/src/tui/package.json +1 -1
  111. package/src/ui/dist/assets/{AiManusChatView-BGLArZRn.js → AiManusChatView-m2FNtwbn.js} +110 -14
  112. package/src/ui/dist/assets/{AnalysisPlugin-BgDGSigG.js → AnalysisPlugin-BMTF8EGL.js} +1 -1
  113. package/src/ui/dist/assets/{AutoFigurePlugin-B65HD7L4.js → AutoFigurePlugin-DxPdMUNb.js} +5 -5
  114. package/src/ui/dist/assets/{CliPlugin-CUqgsFHC.js → CliPlugin-BEOWgxCI.js} +9 -9
  115. package/src/ui/dist/assets/{CodeEditorPlugin-CF5EdvaS.js → CodeEditorPlugin-BCXvjqmb.js} +8 -8
  116. package/src/ui/dist/assets/{CodeViewerPlugin-DEeU063D.js → CodeViewerPlugin-DaJcy3nD.js} +5 -5
  117. package/src/ui/dist/assets/{DocViewerPlugin-Df-FuDlZ.js → DocViewerPlugin-ByfeIq4K.js} +3 -3
  118. package/src/ui/dist/assets/{GitDiffViewerPlugin-RAnNaRxM.js → GitDiffViewerPlugin-Cksf3VZ-.js} +830 -86
  119. package/src/ui/dist/assets/{ImageViewerPlugin-DXJ0ZJGg.js → ImageViewerPlugin-CFz-OsTS.js} +5 -5
  120. package/src/ui/dist/assets/{LabCopilotPanel-BlO-sKsj.js → LabCopilotPanel-CJ1cJzoX.js} +10 -10
  121. package/src/ui/dist/assets/{LabPlugin-BajPZW5v.js → LabPlugin-BF3dVJwa.js} +1 -1
  122. package/src/ui/dist/assets/{LatexPlugin-F1OEol8D.js → LatexPlugin-DDkwZ6Sj.js} +7 -7
  123. package/src/ui/dist/assets/{MarkdownViewerPlugin-MhUupqwT.js → MarkdownViewerPlugin-HAuvurcT.js} +4 -4
  124. package/src/ui/dist/assets/{MarketplacePlugin-DxhIEsv0.js → MarketplacePlugin-BtoTYy2C.js} +3 -3
  125. package/src/ui/dist/assets/{index-B-2scqCJ.js → NotebookEditor-CSJYx7b-.js} +12 -155
  126. package/src/ui/dist/assets/{NotebookEditor-q7TkhewC.js → NotebookEditor-DQgRezm_.js} +1 -1
  127. package/src/ui/dist/assets/{PdfLoader-B8ZOTKFc.js → PdfLoader-DPa_-fv6.js} +1 -1
  128. package/src/ui/dist/assets/{PdfMarkdownPlugin-xFPvzvWh.js → PdfMarkdownPlugin-BZpXOEjm.js} +3 -3
  129. package/src/ui/dist/assets/{PdfViewerPlugin-EjEcsIB8.js → PdfViewerPlugin-BT8a6wGR.js} +10 -10
  130. package/src/ui/dist/assets/{SearchPlugin-ixY-1lgW.js → SearchPlugin-D_blveZi.js} +1 -1
  131. package/src/ui/dist/assets/{Stepper-gYFK2Pgz.js → Stepper-DH2k75Vo.js} +1 -1
  132. package/src/ui/dist/assets/{TextViewerPlugin-Cym6pv_n.js → TextViewerPlugin-Btx0M3hX.js} +4 -4
  133. package/src/ui/dist/assets/{VNCViewer-BPmIHcmK.js → VNCViewer-DImJO4rO.js} +9 -9
  134. package/src/ui/dist/assets/{bibtex-Btv6Wi7f.js → bibtex-B-Hqu0Sg.js} +1 -1
  135. package/src/ui/dist/assets/{code-BlG7g85c.js → code-BUfXGJSl.js} +1 -1
  136. package/src/ui/dist/assets/{file-content-DBT5OfTZ.js → file-content-VqamwI3X.js} +1 -1
  137. package/src/ui/dist/assets/{file-diff-panel-BWXYzqHk.js → file-diff-panel-C_wOoS7a.js} +1 -1
  138. package/src/ui/dist/assets/{file-socket-wDlx6byM.js → file-socket-D2bTuMVP.js} +1 -1
  139. package/src/ui/dist/assets/{file-utils-Ba3nJmH0.js → file-utils--zJCPN1i.js} +1 -1
  140. package/src/ui/dist/assets/{image-BwtCyguk.js → image-BZkGJ4mM.js} +1 -1
  141. package/src/ui/dist/assets/{index-CfRpE209.js → index-CxkvSeKw.js} +2 -2
  142. package/src/ui/dist/assets/{index-DcqvKzeJ.js → index-D9QIGcmc.js} +1 -1
  143. package/src/ui/dist/assets/{index-DpMZw8aM.css → index-DXZ1daiJ.css} +163 -34
  144. package/src/ui/dist/assets/index-DdRW6RMJ.js +159 -0
  145. package/src/ui/dist/assets/{index-Bz5AaWL7.js → index-DjggJovS.js} +2948 -1565
  146. package/src/ui/dist/assets/{message-square-BnlyWVH0.js → message-square-FUIPIhU2.js} +1 -1
  147. package/src/ui/dist/assets/{monaco-CXe0pAVe.js → monaco-DHMc7kKM.js} +1 -1
  148. package/src/ui/dist/assets/{popover-BCHmVhHj.js → popover-B85oCgCS.js} +1 -1
  149. package/src/ui/dist/assets/{project-sync-Brk6kaOD.js → project-sync-DOMCcPac.js} +1 -1
  150. package/src/ui/dist/assets/{sigma-D72eSUep.js → sigma-BO2rQrl3.js} +1 -1
  151. package/src/ui/dist/assets/{tooltip-BMWd0dqX.js → tooltip-B1OspAkx.js} +1 -1
  152. package/src/ui/dist/assets/{trash-BIt_eWIS.js → trash-BsVEH_dV.js} +1 -1
  153. package/src/ui/dist/assets/{useCliAccess-N1hkTRrR.js → useCliAccess-b8L6JuZm.js} +1 -1
  154. package/src/ui/dist/assets/{useFileDiffOverlay-DPRPv6rv.js → useFileDiffOverlay-BY7uA9hV.js} +1 -1
  155. package/src/ui/dist/assets/{wrap-text-E5-UheyP.js → wrap-text-BwyVuUIK.js} +1 -1
  156. package/src/ui/dist/assets/{zoom-out-D4TR-ZZ_.js → zoom-out-RDpLugQP.js} +1 -1
  157. package/src/ui/dist/index.html +5 -2
  158. /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
@@ -448,7 +448,7 @@ def run_monitor(session_dir: Path) -> int:
448
448
  _terminate_process(process, process_group_id)
449
449
  stop_requested = True
450
450
 
451
- if session_kind == "terminal" and output_fd is not None and process.poll() is None:
451
+ if output_fd is not None and process.poll() is None:
452
452
  cursor_payload = read_json(input_cursor_path, {}) or {}
453
453
  offset = int(cursor_payload.get("offset") or 0)
454
454
  input_entries = read_jsonl(input_path)
@@ -555,6 +555,7 @@ class BashExecService:
555
555
  before_seq: int | None = None,
556
556
  after_seq: int | None = None,
557
557
  order: str = "asc",
558
+ prefer_visible: bool = False,
558
559
  ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
559
560
  if not self.meta_path(quest_root, bash_id).exists():
560
561
  raise FileNotFoundError(f"Unknown bash session `{bash_id}`.")
@@ -579,9 +580,16 @@ class BashExecService:
579
580
  entries = [entry for entry in entries if int(entry.get("seq") or 0) > normalized_after]
580
581
  if normalized_before is not None:
581
582
  entries = [entry for entry in entries if int(entry.get("seq") or 0) < normalized_before]
583
+ selection_pool = entries
584
+ if prefer_visible:
585
+ visible_entries = [
586
+ entry for entry in entries if str(entry.get("stream") or "") not in {"system", "prompt"}
587
+ ]
588
+ if visible_entries:
589
+ selection_pool = visible_entries
582
590
  normalized_limit = max(1, limit)
583
- truncated = len(entries) > normalized_limit
584
- selected = entries[-normalized_limit:]
591
+ truncated = len(selection_pool) > normalized_limit
592
+ selected = selection_pool[-normalized_limit:]
585
593
  if order == "desc":
586
594
  selected = list(reversed(selected))
587
595
  tail_start_seq = int(selected[0].get("seq") or 0) if selected else None
@@ -1025,15 +1033,14 @@ class BashExecService:
1025
1033
  if not normalized_data:
1026
1034
  raise ValueError("terminal_input_required")
1027
1035
  session = self.reconcile_session(quest_root, bash_id)
1028
- if _normalize_string(session.get("kind")).lower() != "terminal":
1029
- raise ValueError("not_terminal_session")
1030
1036
  status = _normalize_string(session.get("status")).lower()
1031
1037
  if status in TERMINAL_STATUSES:
1032
1038
  raise ValueError("terminal_session_inactive")
1033
1039
  runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
1034
- if runtime is None:
1040
+ if runtime is None and _normalize_string(session.get("kind")).lower() == "terminal":
1035
1041
  raise ValueError("terminal_runtime_inactive")
1036
- runtime.write_input(normalized_data)
1042
+ if runtime is not None:
1043
+ runtime.write_input(normalized_data)
1037
1044
 
1038
1045
  entry = {
1039
1046
  "input_id": generate_id("tin"),
@@ -1081,9 +1088,10 @@ class BashExecService:
1081
1088
  return True
1082
1089
 
1083
1090
  def issue_terminal_attach_token(self, quest_root: Path, bash_id: str, *, ttl_seconds: int = 60) -> dict[str, Any]:
1084
- runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
1085
- if runtime is None:
1086
- raise ValueError("terminal_runtime_inactive")
1091
+ session = self.reconcile_session(quest_root, bash_id)
1092
+ status = _normalize_string(session.get("status")).lower()
1093
+ if status in TERMINAL_STATUSES:
1094
+ raise ValueError("terminal_session_inactive")
1087
1095
  token = self._terminal_runtime_manager.issue_attach_token(
1088
1096
  quest_root,
1089
1097
  bash_id,
@@ -293,6 +293,20 @@ class QQRelayChannel(BaseChannel):
293
293
  def matches_profile(item: dict[str, Any], profile_id: str) -> bool:
294
294
  item_profile_id = str(item.get("profile_id") or "").strip()
295
295
  return item_profile_id == profile_id or (not item_profile_id and len(profiles) == 1)
296
+
297
+ def count_profile_records(path: Path, profile_id: str) -> int:
298
+ total = 0
299
+ for raw in read_jsonl(path):
300
+ if not isinstance(raw, dict):
301
+ continue
302
+ record_profile_id = str(raw.get("profile_id") or "").strip()
303
+ if not record_profile_id:
304
+ parsed = parse_conversation_id(str(raw.get("conversation_id") or "").strip())
305
+ record_profile_id = str((parsed or {}).get("profile_id") or "").strip()
306
+ if record_profile_id == profile_id or (not record_profile_id and len(profiles) == 1):
307
+ total += 1
308
+ return total
309
+
296
310
  profile_snapshots = []
297
311
  for profile in profiles:
298
312
  profile_id = str(profile.get("profile_id") or "").strip()
@@ -342,6 +356,9 @@ class QQRelayChannel(BaseChannel):
342
356
  "discovered_targets": profile_targets,
343
357
  "recent_conversations": profile_recent_conversations,
344
358
  "bindings": profile_bindings,
359
+ "inbox_count": count_profile_records(self.inbox_path, profile_id),
360
+ "outbox_count": count_profile_records(self.outbox_path, profile_id),
361
+ "ignored_count": count_profile_records(self.ignored_path, profile_id),
345
362
  "target_count": len(profile_targets),
346
363
  "binding_count": len(profile_bindings),
347
364
  "last_error": gateway_state.get("last_error") if isinstance(gateway_state, dict) else None,
@@ -282,6 +282,19 @@ class GenericRelayChannel(BaseChannel):
282
282
  item_profile_id = str(item.get("profile_id") or "").strip()
283
283
  return item_profile_id == profile_id or (not item_profile_id and len(profiles) == 1)
284
284
 
285
+ def count_profile_records(path: Path, profile_id: str) -> int:
286
+ total = 0
287
+ for raw in read_jsonl(path):
288
+ if not isinstance(raw, dict):
289
+ continue
290
+ record_profile_id = str(raw.get("profile_id") or "").strip()
291
+ if not record_profile_id:
292
+ parsed = parse_conversation_id(str(raw.get("conversation_id") or "").strip())
293
+ record_profile_id = str((parsed or {}).get("profile_id") or "").strip()
294
+ if record_profile_id == profile_id or (not record_profile_id and len(profiles) == 1):
295
+ total += 1
296
+ return total
297
+
285
298
  profile_snapshots = []
286
299
  for profile in profiles:
287
300
  profile_id = str(profile.get("profile_id") or "").strip()
@@ -322,6 +335,9 @@ class GenericRelayChannel(BaseChannel):
322
335
  "discovered_targets": profile_targets,
323
336
  "recent_conversations": profile_recent_conversations,
324
337
  "bindings": profile_bindings,
338
+ "inbox_count": count_profile_records(self.inbox_path, profile_id),
339
+ "outbox_count": count_profile_records(self.outbox_path, profile_id),
340
+ "ignored_count": count_profile_records(self.ignored_path, profile_id),
325
341
  "target_count": len(profile_targets),
326
342
  "binding_count": len(profile_bindings),
327
343
  "last_error": profile_runtime_state.get("last_error") if isinstance(profile_runtime_state, dict) else None,
@@ -236,7 +236,7 @@ def run_command(home: Path, quest_id: str, skill_id: str, message: str, model: s
236
236
  config_manager = ConfigManager(home)
237
237
  config_manager.ensure_files()
238
238
  config = config_manager.load_named("config")
239
- runners = config_manager.load_named("runners")
239
+ runners = config_manager.load_runners_config()
240
240
  quest_root = home / "quests" / quest_id
241
241
  codex_cfg = runners.get("codex", {})
242
242
  logger = JsonlLogger(home / "logs", level=config.get("logging", {}).get("level", "info"))
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  CONFIG_NAMES = ("config", "runners", "connectors", "plugins", "mcp_servers")
7
7
  REQUIRED_CONFIG_NAMES = ("config", "runners", "connectors")
8
8
  OPTIONAL_CONFIG_NAMES = ("plugins", "mcp_servers")
9
+ SYSTEM_CONNECTOR_NAMES = ("qq", "telegram", "discord", "slack", "feishu", "whatsapp", "lingzhu")
9
10
 
10
11
 
11
12
  @dataclass(frozen=True)
@@ -20,6 +21,10 @@ def config_filename(name: str) -> str:
20
21
  return f"{name}.yaml"
21
22
 
22
23
 
24
+ def default_system_enabled_connectors() -> dict[str, bool]:
25
+ return {name: name == "qq" for name in SYSTEM_CONNECTOR_NAMES}
26
+
27
+
23
28
  def default_config(home: Path) -> dict:
24
29
  return {
25
30
  "home": str(home),
@@ -65,6 +70,7 @@ def default_config(home: Path) -> dict:
65
70
  "auto_ack": True,
66
71
  "milestone_push": True,
67
72
  "direct_chat_enabled": True,
73
+ "system_enabled": default_system_enabled_connectors(),
68
74
  },
69
75
  "cloud": {
70
76
  "enabled": False,
@@ -128,7 +134,7 @@ def default_connectors() -> dict:
128
134
  "profiles": [],
129
135
  "app_id": None,
130
136
  "app_secret": None,
131
- "app_secret_env": "QQ_APP_SECRET",
137
+ "app_secret_env": None,
132
138
  "bot_name": "DeepScientist",
133
139
  "command_prefix": "/",
134
140
  "main_chat_id": None,
@@ -149,7 +155,7 @@ def default_connectors() -> dict:
149
155
  "bot_name": "DeepScientist",
150
156
  "command_prefix": "/",
151
157
  "bot_token": None,
152
- "bot_token_env": "TELEGRAM_BOT_TOKEN",
158
+ "bot_token_env": None,
153
159
  "dm_policy": "pairing",
154
160
  "allow_from": [],
155
161
  "group_policy": "open",
@@ -165,7 +171,7 @@ def default_connectors() -> dict:
165
171
  "bot_name": "DeepScientist",
166
172
  "command_prefix": "/",
167
173
  "bot_token": None,
168
- "bot_token_env": "DISCORD_BOT_TOKEN",
174
+ "bot_token_env": None,
169
175
  "application_id": None,
170
176
  "dm_policy": "pairing",
171
177
  "allow_from": [],
@@ -183,10 +189,10 @@ def default_connectors() -> dict:
183
189
  "bot_name": "DeepScientist",
184
190
  "command_prefix": "/",
185
191
  "bot_token": None,
186
- "bot_token_env": "SLACK_BOT_TOKEN",
192
+ "bot_token_env": None,
187
193
  "bot_user_id": None,
188
194
  "app_token": None,
189
- "app_token_env": "SLACK_APP_TOKEN",
195
+ "app_token_env": None,
190
196
  "dm_policy": "pairing",
191
197
  "allow_from": [],
192
198
  "group_policy": "open",
@@ -203,7 +209,7 @@ def default_connectors() -> dict:
203
209
  "command_prefix": "/",
204
210
  "app_id": None,
205
211
  "app_secret": None,
206
- "app_secret_env": "FEISHU_APP_SECRET",
212
+ "app_secret_env": None,
207
213
  "api_base_url": "https://open.feishu.cn",
208
214
  "dm_policy": "pairing",
209
215
  "allow_from": [],
@@ -30,12 +30,14 @@ from ..qq_profiles import (
30
30
  qq_profile_label,
31
31
  )
32
32
  from ..network import urlopen_with_proxy as urlopen
33
+ from ..runners.runtime_overrides import apply_codex_runtime_overrides, apply_runners_runtime_overrides
33
34
  from ..shared import read_json, read_text, read_yaml, resolve_runner_binary, run_command, sha256_text, utc_now, which, write_text, write_yaml
34
35
  from .models import (
35
36
  CONFIG_NAMES,
36
37
  OPTIONAL_CONFIG_NAMES,
37
38
  REQUIRED_CONFIG_NAMES,
38
39
  ConfigFileInfo,
40
+ SYSTEM_CONNECTOR_NAMES,
39
41
  config_filename,
40
42
  default_payload,
41
43
  )
@@ -91,6 +93,36 @@ class ConfigManager:
91
93
  def load_named_normalized(self, name: str, create_optional: bool = False) -> dict:
92
94
  return self._normalize_named_payload(name, self.load_named(name, create_optional=create_optional))
93
95
 
96
+ def load_runners_config(self) -> dict:
97
+ return apply_runners_runtime_overrides(self.load_named_normalized("runners"))
98
+
99
+ def load_runtime_config(self) -> dict:
100
+ return self.load_named_normalized("config")
101
+
102
+ def system_connector_gates(self) -> dict[str, bool]:
103
+ config = self.load_runtime_config()
104
+ connectors = config.get("connectors") if isinstance(config.get("connectors"), dict) else {}
105
+ system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
106
+ return {
107
+ name: self._coerce_bool(system_enabled.get(name), default=name == "qq")
108
+ for name in SYSTEM_CONNECTOR_NAMES
109
+ }
110
+
111
+ def system_enabled_connector_names(self) -> list[str]:
112
+ gates = self.system_connector_gates()
113
+ return [name for name in SYSTEM_CONNECTOR_NAMES if gates.get(name, False)]
114
+
115
+ def is_connector_system_enabled(self, name: str) -> bool:
116
+ normalized = str(name or "").strip().lower()
117
+ if not normalized:
118
+ return False
119
+ if normalized == "local":
120
+ return True
121
+ gates = self.system_connector_gates()
122
+ if normalized in gates:
123
+ return gates[normalized]
124
+ return True
125
+
94
126
  def load_named_text(self, name: str, create_optional: bool = False) -> str:
95
127
  path = self.path_for(name)
96
128
  if create_optional and name in OPTIONAL_CONFIG_NAMES and not path.exists():
@@ -1039,9 +1071,16 @@ Use **Test** when the file exposes runtime dependencies.
1039
1071
  )
1040
1072
 
1041
1073
  def _probe_codex_runner(self, config: dict) -> dict:
1074
+ config = apply_codex_runtime_overrides(config)
1042
1075
  checked_at = utc_now()
1043
1076
  binary = str(config.get("binary") or "codex").strip() or "codex"
1044
1077
  resolved_binary = resolve_runner_binary(binary, runner_name="codex")
1078
+ raw_reasoning_effort = config.get("model_reasoning_effort")
1079
+ reasoning_effort = (
1080
+ str(raw_reasoning_effort).strip()
1081
+ if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
1082
+ else ("xhigh" if raw_reasoning_effort is None else None)
1083
+ )
1045
1084
  details: dict[str, object] = {
1046
1085
  "binary": binary,
1047
1086
  "resolved_binary": resolved_binary,
@@ -1049,7 +1088,7 @@ Use **Test** when the file exposes runtime dependencies.
1049
1088
  "model": str(config.get("model") or "gpt-5.4"),
1050
1089
  "approval_policy": str(config.get("approval_policy") or "on-request"),
1051
1090
  "sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
1052
- "reasoning_effort": str(config.get("model_reasoning_effort") or "xhigh"),
1091
+ "reasoning_effort": reasoning_effort,
1053
1092
  "checked_at": checked_at,
1054
1093
  }
1055
1094
  if not resolved_binary:
@@ -1082,7 +1121,6 @@ Use **Test** when the file exposes runtime dependencies.
1082
1121
  approval_policy = str(config.get("approval_policy") or "on-request").strip()
1083
1122
  if approval_policy:
1084
1123
  command.extend(["-c", f'approval_policy="{approval_policy}"'])
1085
- reasoning_effort = str(config.get("model_reasoning_effort") or "xhigh").strip()
1086
1124
  if reasoning_effort:
1087
1125
  command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
1088
1126
  sandbox_mode = str(config.get("sandbox_mode") or "workspace-write").strip()
@@ -1373,6 +1411,8 @@ Use **Test** when the file exposes runtime dependencies.
1373
1411
  normalized = self._deep_merge(defaults, payload)
1374
1412
  bootstrap = normalized.get("bootstrap") if isinstance(normalized.get("bootstrap"), dict) else {}
1375
1413
  raw_bootstrap = payload.get("bootstrap") if isinstance(payload.get("bootstrap"), dict) else {}
1414
+ connectors = normalized.get("connectors") if isinstance(normalized.get("connectors"), dict) else {}
1415
+ raw_connectors = payload.get("connectors") if isinstance(payload.get("connectors"), dict) else {}
1376
1416
  default_locale = str(defaults.get("default_locale") or "").strip()
1377
1417
  current_locale = str(normalized.get("default_locale") or "").strip()
1378
1418
  locale_source = str(raw_bootstrap.get("locale_source") or "").strip().lower()
@@ -1396,6 +1436,25 @@ Use **Test** when the file exposes runtime dependencies.
1396
1436
  bootstrap["locale_initialized_at"] = bootstrap.get("locale_initialized_at")
1397
1437
  bootstrap["locale_initialized_browser_locale"] = bootstrap.get("locale_initialized_browser_locale")
1398
1438
  normalized["bootstrap"] = bootstrap
1439
+ raw_system_enabled = raw_connectors.get("system_enabled") if isinstance(raw_connectors.get("system_enabled"), dict) else {}
1440
+ default_system_enabled = (
1441
+ defaults.get("connectors", {}).get("system_enabled")
1442
+ if isinstance(defaults.get("connectors"), dict)
1443
+ else {}
1444
+ )
1445
+ current_system_enabled = (
1446
+ connectors.get("system_enabled")
1447
+ if isinstance(connectors.get("system_enabled"), dict)
1448
+ else {}
1449
+ )
1450
+ connectors["system_enabled"] = {
1451
+ name: self._coerce_bool(
1452
+ raw_system_enabled.get(name, current_system_enabled.get(name)),
1453
+ default=bool(default_system_enabled.get(name, False)),
1454
+ )
1455
+ for name in SYSTEM_CONNECTOR_NAMES
1456
+ }
1457
+ normalized["connectors"] = connectors
1399
1458
  return normalized
1400
1459
 
1401
1460
  @staticmethod
@@ -1408,6 +1467,20 @@ Use **Test** when the file exposes runtime dependencies.
1408
1467
  return False
1409
1468
  return abs(initial - 1.0) < 1e-9 and abs(multiplier - 2.0) < 1e-9 and abs(max_backoff - 8.0) < 1e-9
1410
1469
 
1470
+ @staticmethod
1471
+ def _coerce_bool(value: object, *, default: bool = False) -> bool:
1472
+ if value is None:
1473
+ return default
1474
+ if isinstance(value, bool):
1475
+ return value
1476
+ if isinstance(value, str):
1477
+ normalized = value.strip().lower()
1478
+ if normalized in {"1", "true", "yes", "on", "y"}:
1479
+ return True
1480
+ if normalized in {"0", "false", "no", "off", "n", ""}:
1481
+ return False
1482
+ return bool(value)
1483
+
1411
1484
  def _normalize_plugins_payload(self, payload: dict) -> dict:
1412
1485
  normalized = deepcopy(payload)
1413
1486
  if "load_paths" not in normalized and isinstance(normalized.get("search_paths"), list):
@@ -10,6 +10,13 @@ from .shared import slugify
10
10
  PROFILEABLE_CONNECTOR_NAMES = ("telegram", "discord", "slack", "feishu", "whatsapp")
11
11
 
12
12
 
13
+ def _normalize_secret_pair(payload: dict[str, Any], direct_key: str, env_key: str) -> None:
14
+ direct = _as_text(payload.get(direct_key))
15
+ env_name = _as_text(payload.get(env_key))
16
+ payload[direct_key] = direct
17
+ payload[env_key] = None if direct else env_name
18
+
19
+
13
20
  CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
14
21
  "telegram": {
15
22
  "profile_id_prefix": "telegram-profile",
@@ -35,7 +42,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
35
42
  "transport": "polling",
36
43
  "bot_name": "DeepScientist",
37
44
  "bot_token": None,
38
- "bot_token_env": "TELEGRAM_BOT_TOKEN",
45
+ "bot_token_env": None,
39
46
  },
40
47
  "profile_fields": (
41
48
  "enabled",
@@ -47,6 +54,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
47
54
  "migration_keys": ("bot_token",),
48
55
  "label_fields": ("bot_name",),
49
56
  "id_fields": ("bot_name",),
57
+ "secret_pairs": (("bot_token", "bot_token_env"),),
50
58
  },
51
59
  "discord": {
52
60
  "profile_id_prefix": "discord-profile",
@@ -74,7 +82,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
74
82
  "transport": "gateway",
75
83
  "bot_name": "DeepScientist",
76
84
  "bot_token": None,
77
- "bot_token_env": "DISCORD_BOT_TOKEN",
85
+ "bot_token_env": None,
78
86
  "application_id": None,
79
87
  },
80
88
  "profile_fields": (
@@ -88,6 +96,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
88
96
  "migration_keys": ("bot_token", "application_id"),
89
97
  "label_fields": ("bot_name", "application_id"),
90
98
  "id_fields": ("application_id", "bot_name"),
99
+ "secret_pairs": (("bot_token", "bot_token_env"),),
91
100
  },
92
101
  "slack": {
93
102
  "profile_id_prefix": "slack-profile",
@@ -116,10 +125,10 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
116
125
  "transport": "socket_mode",
117
126
  "bot_name": "DeepScientist",
118
127
  "bot_token": None,
119
- "bot_token_env": "SLACK_BOT_TOKEN",
128
+ "bot_token_env": None,
120
129
  "bot_user_id": None,
121
130
  "app_token": None,
122
- "app_token_env": "SLACK_APP_TOKEN",
131
+ "app_token_env": None,
123
132
  },
124
133
  "profile_fields": (
125
134
  "enabled",
@@ -134,6 +143,10 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
134
143
  "migration_keys": ("bot_token", "bot_user_id", "app_token"),
135
144
  "label_fields": ("bot_name", "bot_user_id"),
136
145
  "id_fields": ("bot_user_id", "bot_name"),
146
+ "secret_pairs": (
147
+ ("bot_token", "bot_token_env"),
148
+ ("app_token", "app_token_env"),
149
+ ),
137
150
  },
138
151
  "feishu": {
139
152
  "profile_id_prefix": "feishu-profile",
@@ -162,7 +175,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
162
175
  "bot_name": "DeepScientist",
163
176
  "app_id": None,
164
177
  "app_secret": None,
165
- "app_secret_env": "FEISHU_APP_SECRET",
178
+ "app_secret_env": None,
166
179
  "api_base_url": "https://open.feishu.cn",
167
180
  },
168
181
  "profile_fields": (
@@ -177,6 +190,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
177
190
  "migration_keys": ("app_id", "app_secret"),
178
191
  "label_fields": ("bot_name", "app_id"),
179
192
  "id_fields": ("app_id", "bot_name"),
193
+ "secret_pairs": (("app_secret", "app_secret_env"),),
180
194
  },
181
195
  "whatsapp": {
182
196
  "profile_id_prefix": "whatsapp-profile",
@@ -213,6 +227,7 @@ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
213
227
  "migration_keys": ("session_dir",),
214
228
  "label_fields": ("bot_name",),
215
229
  "id_fields": ("bot_name",),
230
+ "secret_pairs": (),
216
231
  },
217
232
  }
218
233
 
@@ -270,10 +285,17 @@ def normalize_connector_config(connector_name: str, config: dict[str, Any] | Non
270
285
  if key in payload
271
286
  }
272
287
  shared["profiles"] = []
288
+ for direct_key, env_key in spec.get("secret_pairs", ()):
289
+ _normalize_secret_pair(shared, direct_key, env_key)
273
290
 
274
291
  raw_profiles = payload.get("profiles")
275
292
  items = list(raw_profiles) if isinstance(raw_profiles, list) else []
276
- if not items and any(_as_text(payload.get(key)) for key in spec["migration_keys"]):
293
+ has_direct_migration_value = any(_as_text(payload.get(key)) for key in spec["migration_keys"])
294
+ has_env_only_secret = bool(payload.get("enabled")) and any(
295
+ _as_text(payload.get(env_key))
296
+ for _, env_key in spec.get("secret_pairs", ())
297
+ )
298
+ if not items and (has_direct_migration_value or has_env_only_secret):
277
299
  items = [{key: payload.get(key) for key in spec["profile_fields"]}]
278
300
 
279
301
  used_ids: set[str] = set()
@@ -297,6 +319,8 @@ def normalize_connector_config(connector_name: str, config: dict[str, Any] | Non
297
319
  current["transport"] = infer_connector_transport(connector_name, current)
298
320
  if "mode" in spec["profile_defaults"] or current.get("mode") is not None:
299
321
  current["mode"] = _as_text(current.get("mode")) or str(spec["profile_defaults"].get("mode") or "")
322
+ for direct_key, env_key in spec.get("secret_pairs", ()):
323
+ _normalize_secret_pair(current, direct_key, env_key)
300
324
  current["profile_id"] = _unique_profile_id(
301
325
  _profile_seed(connector_name, current, index=index),
302
326
  prefix=str(spec["profile_id_prefix"]),
@@ -309,6 +333,10 @@ def normalize_connector_config(connector_name: str, config: dict[str, Any] | Non
309
333
  if len(profiles) == 1:
310
334
  for key in spec["profile_fields"]:
311
335
  shared[key] = profiles[0].get(key)
336
+ elif len(profiles) > 1:
337
+ for direct_key, env_key in spec.get("secret_pairs", ()):
338
+ shared[direct_key] = None
339
+ shared[env_key] = None
312
340
  return shared
313
341
 
314
342