@researai/deepscientist 1.5.15 → 1.5.17

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 (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -1,13 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import ipaddress
3
4
  import json
4
5
  import re
6
+ import shutil
5
7
  import subprocess
6
8
  import tomllib
7
9
  from functools import lru_cache
10
+ from pathlib import Path
11
+ from urllib.parse import urlparse
12
+
13
+ from .shared import ensure_dir, read_text, write_text
8
14
 
9
15
  _MIN_XHIGH_SUPPORTED_VERSION = (0, 63, 0)
16
+ _CHAT_WIRE_COMPAT_VERSION = (0, 57, 0)
10
17
  _CODEX_VERSION_PATTERN = re.compile(r"codex-cli\s+(\d+)\.(\d+)\.(\d+)", re.IGNORECASE)
18
+ _CODEX_HOME_SYNCED_FILES = ("config.toml", "auth.json")
19
+ _CODEX_HOME_SYNCED_DIRS = ("skills", "agents", "prompts")
20
+ _CODEX_HOME_QUEST_OVERLAY_DIRS = ("skills", "prompts")
21
+ _ROOT_TABLE_SECTION_PATTERN = re.compile(r"^\s*\[")
22
+ _ROOT_MODEL_ASSIGNMENT_PATTERN = re.compile(r"^\s*(model_provider|model)\s*=")
23
+ _COMPAT_BEGIN_MARKER = "# BEGIN DEEPSCIENTIST PROFILE COMPAT"
24
+ _COMPAT_END_MARKER = "# END DEEPSCIENTIST PROFILE COMPAT"
25
+ _MISSING_ENV_PATTERN = re.compile(r"Missing environment variable:\s*[`'\"]?([^`'\"\s]+)", re.IGNORECASE)
26
+ _LOCAL_PROVIDER_HOST_ALIASES = {"localhost", "host.docker.internal"}
11
27
 
12
28
 
13
29
  def parse_codex_cli_version(text: str) -> tuple[int, int, int] | None:
@@ -41,6 +57,50 @@ def format_codex_cli_version(version: tuple[int, int, int] | None) -> str:
41
57
  return ".".join(str(part) for part in version)
42
58
 
43
59
 
60
+ def chat_wire_compatible_codex_version() -> tuple[int, int, int]:
61
+ return _CHAT_WIRE_COMPAT_VERSION
62
+
63
+
64
+ def _split_root_table_lines(config_text: str) -> tuple[list[str], list[str]]:
65
+ lines = str(config_text or "").splitlines()
66
+ for index, line in enumerate(lines):
67
+ if _ROOT_TABLE_SECTION_PATTERN.match(line):
68
+ return lines[:index], lines[index:]
69
+ return lines, []
70
+
71
+
72
+ def _strip_root_model_assignments(lines: list[str]) -> list[str]:
73
+ filtered: list[str] = []
74
+ skipping_compat_block = False
75
+ for line in lines:
76
+ stripped = line.strip()
77
+ if stripped == _COMPAT_BEGIN_MARKER:
78
+ skipping_compat_block = True
79
+ continue
80
+ if skipping_compat_block:
81
+ if stripped == _COMPAT_END_MARKER:
82
+ skipping_compat_block = False
83
+ continue
84
+ if _ROOT_MODEL_ASSIGNMENT_PATTERN.match(line):
85
+ continue
86
+ filtered.append(line)
87
+ while filtered and not filtered[0].strip():
88
+ filtered.pop(0)
89
+ while filtered and not filtered[-1].strip():
90
+ filtered.pop()
91
+ return filtered
92
+
93
+
94
+ def _join_field_names(fields: list[str]) -> str:
95
+ if not fields:
96
+ return ""
97
+ if len(fields) == 1:
98
+ return fields[0]
99
+ if len(fields) == 2:
100
+ return f"{fields[0]} and {fields[1]}"
101
+ return ", ".join(fields[:-1]) + f", and {fields[-1]}"
102
+
103
+
44
104
  def normalize_codex_reasoning_effort(
45
105
  reasoning_effort: str | None,
46
106
  *,
@@ -86,32 +146,317 @@ def adapt_profile_only_provider_config(
86
146
  if not isinstance(profile_payload, dict):
87
147
  return config_text, None
88
148
 
89
- prefix_lines: list[str] = []
90
- injected_fields: list[str] = []
91
- if "model_provider" not in parsed:
92
- model_provider = str(profile_payload.get("model_provider") or "").strip()
93
- if model_provider:
94
- prefix_lines.append(f"model_provider = {json.dumps(model_provider, ensure_ascii=False)}")
95
- injected_fields.append("model_provider")
96
- if "model" not in parsed:
97
- model = str(profile_payload.get("model") or "").strip()
98
- if model:
99
- prefix_lines.append(f"model = {json.dumps(model, ensure_ascii=False)}")
100
- injected_fields.append("model")
101
-
102
- if not prefix_lines:
149
+ profile_model_provider = str(profile_payload.get("model_provider") or "").strip()
150
+ profile_model = str(profile_payload.get("model") or "").strip()
151
+ top_level_model_provider = str(parsed.get("model_provider") or "").strip()
152
+ top_level_model = str(parsed.get("model") or "").strip()
153
+
154
+ root_lines: list[str] = []
155
+ changed_fields: list[str] = []
156
+ conflicted_fields: list[str] = []
157
+ if profile_model_provider and top_level_model_provider != profile_model_provider:
158
+ root_lines.append(f"model_provider = {json.dumps(profile_model_provider, ensure_ascii=False)}")
159
+ changed_fields.append("model_provider")
160
+ if top_level_model_provider:
161
+ conflicted_fields.append("model_provider")
162
+ elif profile_model_provider:
163
+ root_lines.append(f"model_provider = {json.dumps(profile_model_provider, ensure_ascii=False)}")
164
+ if profile_model and top_level_model != profile_model:
165
+ root_lines.append(f"model = {json.dumps(profile_model, ensure_ascii=False)}")
166
+ changed_fields.append("model")
167
+ if top_level_model:
168
+ conflicted_fields.append("model")
169
+ elif profile_model:
170
+ root_lines.append(f"model = {json.dumps(profile_model, ensure_ascii=False)}")
171
+
172
+ if not changed_fields:
103
173
  return config_text, None
104
174
 
105
- adapted = (
106
- "# BEGIN DEEPSCIENTIST PROFILE COMPAT\n"
107
- + "\n".join(prefix_lines)
108
- + "\n# END DEEPSCIENTIST PROFILE COMPAT\n\n"
109
- + config_text.lstrip()
110
- )
175
+ root_prefix, body_lines = _split_root_table_lines(config_text)
176
+ cleaned_root = _strip_root_model_assignments(root_prefix)
177
+ adapted_lines: list[str] = [
178
+ _COMPAT_BEGIN_MARKER,
179
+ *root_lines,
180
+ _COMPAT_END_MARKER,
181
+ ]
182
+ if cleaned_root:
183
+ adapted_lines.append("")
184
+ adapted_lines.extend(cleaned_root)
185
+ if body_lines:
186
+ adapted_lines.append("")
187
+ adapted_lines.extend(body_lines)
188
+ adapted = "\n".join(adapted_lines).rstrip() + "\n"
189
+ field_text = _join_field_names(changed_fields)
111
190
  return (
112
191
  adapted,
113
192
  (
114
- f"DeepScientist promoted `{normalized_profile}` profile "
115
- f"{', '.join(injected_fields)} to the top level for Codex compatibility."
193
+ f"DeepScientist overrode conflicting top-level {field_text} with values from profile "
194
+ f"`{normalized_profile}` for Codex compatibility."
195
+ if conflicted_fields
196
+ else f"DeepScientist promoted `{normalized_profile}` profile {field_text} to the top level for Codex compatibility."
116
197
  ),
117
198
  )
199
+
200
+
201
+ def _remove_tree_path(path: Path) -> None:
202
+ if not path.exists() and not path.is_symlink():
203
+ return
204
+ if path.is_symlink() or path.is_file():
205
+ path.unlink()
206
+ return
207
+ shutil.rmtree(path)
208
+
209
+
210
+ def _overlay_file_sources(*roots: Path) -> dict[Path, Path]:
211
+ merged: dict[Path, Path] = {}
212
+ for root in roots:
213
+ if not root.exists() or not root.is_dir():
214
+ continue
215
+ for source_path in sorted(root.rglob("*")):
216
+ if not source_path.is_file():
217
+ continue
218
+ merged[source_path.relative_to(root)] = source_path
219
+ return merged
220
+
221
+
222
+ def _sync_overlay_directory(target_dir: Path, *source_dirs: Path) -> None:
223
+ desired_files = _overlay_file_sources(*source_dirs)
224
+ if not desired_files:
225
+ _remove_tree_path(target_dir)
226
+ return
227
+
228
+ desired_dirs: set[Path] = {Path(".")}
229
+ for relative in desired_files:
230
+ parent = relative.parent
231
+ while True:
232
+ desired_dirs.add(parent)
233
+ if parent == Path("."):
234
+ break
235
+ parent = parent.parent
236
+
237
+ if target_dir.exists() or target_dir.is_symlink():
238
+ for existing_path in sorted(target_dir.rglob("*"), reverse=True):
239
+ relative = existing_path.relative_to(target_dir)
240
+ if existing_path.is_dir() and not existing_path.is_symlink():
241
+ if relative not in desired_dirs:
242
+ shutil.rmtree(existing_path)
243
+ continue
244
+ if relative not in desired_files:
245
+ existing_path.unlink()
246
+
247
+ ensure_dir(target_dir)
248
+ for relative in sorted(desired_dirs, key=lambda item: (len(item.parts), item.as_posix())):
249
+ if relative == Path("."):
250
+ continue
251
+ current = target_dir / relative
252
+ if current.exists() and (current.is_symlink() or current.is_file()):
253
+ current.unlink()
254
+ ensure_dir(current)
255
+
256
+ for relative, source_path in desired_files.items():
257
+ target_path = target_dir / relative
258
+ if target_path.exists() and target_path.is_dir() and not target_path.is_symlink():
259
+ shutil.rmtree(target_path)
260
+ ensure_dir(target_path.parent)
261
+ try:
262
+ same_path = source_path.resolve() == target_path.resolve()
263
+ except FileNotFoundError:
264
+ same_path = False
265
+ if same_path:
266
+ continue
267
+ shutil.copy2(source_path, target_path)
268
+
269
+
270
+ def materialize_codex_runtime_home(
271
+ *,
272
+ source_home: str | Path,
273
+ target_home: str | Path,
274
+ profile: str = "",
275
+ quest_codex_root: str | Path | None = None,
276
+ ) -> str | None:
277
+ source_root = Path(source_home).expanduser()
278
+ target_root = ensure_dir(Path(target_home))
279
+
280
+ for filename in _CODEX_HOME_SYNCED_FILES:
281
+ source_path = source_root / filename
282
+ target_path = target_root / filename
283
+ if not source_path.exists():
284
+ _remove_tree_path(target_path)
285
+ continue
286
+ if target_path.exists() and target_path.is_dir() and not target_path.is_symlink():
287
+ shutil.rmtree(target_path)
288
+ ensure_dir(target_path.parent)
289
+ try:
290
+ same_path = source_path.resolve() == target_path.resolve()
291
+ except FileNotFoundError:
292
+ same_path = False
293
+ if not same_path:
294
+ shutil.copy2(source_path, target_path)
295
+
296
+ overlay_root = Path(quest_codex_root) if quest_codex_root is not None else None
297
+ for dirname in _CODEX_HOME_SYNCED_DIRS:
298
+ overlay_dir = overlay_root / dirname if overlay_root is not None and dirname in _CODEX_HOME_QUEST_OVERLAY_DIRS else None
299
+ source_dirs: list[Path] = [source_root / dirname]
300
+ if overlay_dir is not None:
301
+ source_dirs.append(overlay_dir)
302
+ _sync_overlay_directory(target_root / dirname, *source_dirs)
303
+
304
+ warning: str | None = None
305
+ config_path = target_root / "config.toml"
306
+ if profile and config_path.exists():
307
+ adapted_text, warning = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
308
+ write_text(config_path, adapted_text)
309
+ return warning
310
+
311
+
312
+ def _empty_provider_metadata() -> dict[str, str | bool | None]:
313
+ return {
314
+ "provider": None,
315
+ "model": None,
316
+ "env_key": None,
317
+ "base_url": None,
318
+ "wire_api": None,
319
+ "requires_openai_auth": None,
320
+ }
321
+
322
+
323
+ def active_provider_metadata(
324
+ config_text: str,
325
+ *,
326
+ profile: str | None = None,
327
+ ) -> dict[str, str | bool | None]:
328
+ normalized_profile = str(profile or "").strip()
329
+ if not str(config_text or "").strip():
330
+ return _empty_provider_metadata()
331
+ try:
332
+ parsed = tomllib.loads(config_text)
333
+ except tomllib.TOMLDecodeError:
334
+ return _empty_provider_metadata()
335
+
336
+ profile_payload: dict | None = None
337
+ if normalized_profile:
338
+ profiles = parsed.get("profiles")
339
+ if not isinstance(profiles, dict):
340
+ return _empty_provider_metadata()
341
+ candidate_profile = profiles.get(normalized_profile)
342
+ if not isinstance(candidate_profile, dict):
343
+ return _empty_provider_metadata()
344
+ profile_payload = candidate_profile
345
+
346
+ model_provider = str(
347
+ (profile_payload or {}).get("model_provider")
348
+ or parsed.get("model_provider")
349
+ or ""
350
+ ).strip() or None
351
+ model = str(
352
+ (profile_payload or {}).get("model")
353
+ or parsed.get("model")
354
+ or ""
355
+ ).strip() or None
356
+ provider_payload = None
357
+ model_providers = parsed.get("model_providers")
358
+ if model_provider and isinstance(model_providers, dict):
359
+ candidate = model_providers.get(model_provider)
360
+ if isinstance(candidate, dict):
361
+ provider_payload = candidate
362
+
363
+ env_key = (
364
+ str(provider_payload.get("env_key") or "").strip()
365
+ if isinstance(provider_payload, dict)
366
+ else None
367
+ ) or None
368
+ base_url = (
369
+ str(provider_payload.get("base_url") or "").strip()
370
+ if isinstance(provider_payload, dict)
371
+ else None
372
+ ) or None
373
+ wire_api = (
374
+ str(provider_payload.get("wire_api") or "").strip()
375
+ if isinstance(provider_payload, dict)
376
+ else None
377
+ ) or None
378
+ requires_openai_auth = (
379
+ bool(provider_payload.get("requires_openai_auth"))
380
+ if isinstance(provider_payload, dict) and "requires_openai_auth" in provider_payload
381
+ else None
382
+ )
383
+
384
+ return {
385
+ "provider": model_provider,
386
+ "model": model,
387
+ "env_key": env_key,
388
+ "base_url": base_url,
389
+ "wire_api": wire_api,
390
+ "requires_openai_auth": requires_openai_auth,
391
+ }
392
+
393
+
394
+ def provider_profile_metadata(
395
+ config_text: str,
396
+ *,
397
+ profile: str,
398
+ ) -> dict[str, str | bool | None]:
399
+ normalized_profile = str(profile or "").strip()
400
+ if not normalized_profile:
401
+ return _empty_provider_metadata()
402
+ return active_provider_metadata(config_text, profile=normalized_profile)
403
+
404
+
405
+ def provider_profile_metadata_from_home(
406
+ config_home: str | Path,
407
+ *,
408
+ profile: str,
409
+ ) -> dict[str, str | bool | None]:
410
+ config_path = Path(config_home).expanduser() / "config.toml"
411
+ if not config_path.exists():
412
+ return _empty_provider_metadata()
413
+ return provider_profile_metadata(config_path.read_text(encoding="utf-8"), profile=profile)
414
+
415
+
416
+ def provider_base_url_looks_local(base_url: str | None) -> bool:
417
+ normalized = str(base_url or "").strip()
418
+ if not normalized:
419
+ return False
420
+ parsed = urlparse(normalized)
421
+ hostname = str(parsed.hostname or "").strip().lower()
422
+ if not hostname:
423
+ return False
424
+ if hostname in _LOCAL_PROVIDER_HOST_ALIASES or hostname.endswith(".local"):
425
+ return True
426
+ try:
427
+ ip = ipaddress.ip_address(hostname)
428
+ except ValueError:
429
+ return False
430
+ return ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_unspecified
431
+
432
+
433
+ def missing_provider_env_key(
434
+ metadata: dict[str, str | bool | None],
435
+ env: dict[str, str] | None,
436
+ ) -> str | None:
437
+ env_key = str((metadata or {}).get("env_key") or "").strip()
438
+ if not env_key:
439
+ return None
440
+ env_value = str((env or {}).get(env_key) or "").strip()
441
+ if env_value:
442
+ return None
443
+ return env_key
444
+
445
+
446
+ def missing_provider_env_key_from_text(*texts: str) -> str | None:
447
+ for text in texts:
448
+ match = _MISSING_ENV_PATTERN.search(str(text or ""))
449
+ if match:
450
+ return str(match.group(1) or "").strip() or None
451
+ return None
452
+
453
+
454
+ def active_provider_metadata_from_home(
455
+ config_home: str | Path,
456
+ *,
457
+ profile: str | None = None,
458
+ ) -> dict[str, str | bool | None]:
459
+ config_path = Path(config_home).expanduser() / "config.toml"
460
+ if not config_path.exists():
461
+ return _empty_provider_metadata()
462
+ return active_provider_metadata(config_path.read_text(encoding="utf-8"), profile=profile)
@@ -38,6 +38,7 @@ def default_config(home: Path) -> dict:
38
38
  "ui": {
39
39
  "host": "0.0.0.0",
40
40
  "port": 20999,
41
+ "auth_enabled": False,
41
42
  "auto_open_browser": True,
42
43
  "default_mode": "web",
43
44
  },
@@ -96,7 +97,7 @@ def default_runners() -> dict:
96
97
  "binary": "codex",
97
98
  "config_dir": "~/.codex",
98
99
  "profile": "",
99
- "model": "gpt-5.4",
100
+ "model": "inherit",
100
101
  "model_reasoning_effort": "xhigh",
101
102
  "approval_policy": "never",
102
103
  "sandbox_mode": "danger-full-access",