@researai/deepscientist 1.5.16 → 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.
- package/README.md +66 -23
- package/bin/ds.js +550 -19
- package/docs/en/00_QUICK_START.md +65 -5
- package/docs/en/01_SETTINGS_REFERENCE.md +1 -1
- package/docs/en/09_DOCTOR.md +14 -3
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +12 -3
- package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
- package/docs/en/91_DEVELOPMENT.md +237 -0
- package/docs/en/README.md +7 -3
- package/docs/zh/00_QUICK_START.md +54 -5
- package/docs/zh/01_SETTINGS_REFERENCE.md +1 -1
- package/docs/zh/09_DOCTOR.md +15 -4
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +12 -3
- package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
- package/docs/zh/README.md +7 -3
- package/install.sh +46 -4
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/bridges/connectors.py +8 -2
- package/src/deepscientist/codex_cli_compat.py +185 -72
- package/src/deepscientist/config/service.py +154 -6
- package/src/deepscientist/daemon/api/handlers.py +130 -25
- package/src/deepscientist/daemon/api/router.py +5 -0
- package/src/deepscientist/daemon/app.py +446 -22
- package/src/deepscientist/diagnostics/__init__.py +6 -0
- package/src/deepscientist/diagnostics/runner_failures.py +130 -0
- package/src/deepscientist/doctor.py +207 -3
- package/src/deepscientist/prompts/builder.py +22 -4
- package/src/deepscientist/quest/service.py +413 -13
- package/src/deepscientist/runners/codex.py +59 -14
- package/src/deepscientist/shared.py +19 -0
- package/src/prompts/contracts/shared_interaction.md +3 -2
- package/src/prompts/system.md +13 -0
- package/src/prompts/system_copilot.md +13 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-COFACy7V.js → AiManusChatView-Bv-Z8YpU.js} +44 -44
- package/src/ui/dist/assets/{AnalysisPlugin-DnSm0GZn.js → AnalysisPlugin-BCKAfjba.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-CvwCmDQ5.js → CliPlugin-BCKcpc35.js} +4 -4
- package/src/ui/dist/assets/{CodeEditorPlugin-cOqSa0xq.js → CodeEditorPlugin-DbOfSJ8K.js} +1 -1
- package/src/ui/dist/assets/{CodeViewerPlugin-itb0tltR.js → CodeViewerPlugin-CbaFRrUU.js} +3 -3
- package/src/ui/dist/assets/{DocViewerPlugin-DqKkiCI6.js → DocViewerPlugin-DAjLVeQD.js} +3 -3
- package/src/ui/dist/assets/{GitCommitViewerPlugin-DVgNHBCS.js → GitCommitViewerPlugin-CIUqbUDO.js} +1 -1
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DxL2ezFG.js → GitDiffViewerPlugin-CQACjoAA.js} +1 -1
- package/src/ui/dist/assets/{GitSnapshotViewer-B_RQm1YZ.js → GitSnapshotViewer-0r4nLPke.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-tHqlXY3n.js → ImageViewerPlugin-nBOmI2v_.js} +3 -3
- package/src/ui/dist/assets/{LabCopilotPanel-ClMbq5Yu.js → LabCopilotPanel-BHxOxF4z.js} +1 -1
- package/src/ui/dist/assets/{LabPlugin-L_SuE8ow.js → LabPlugin-BKoZGs95.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-B495DTXC.js → LatexPlugin-ZwtV8pIp.js} +1 -1
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DG28-61B.js → MarkdownViewerPlugin-DKqVfKyW.js} +3 -3
- package/src/ui/dist/assets/{MarketplacePlugin-BiOGT-Kj.js → MarketplacePlugin-BwxStZ9D.js} +1 -1
- package/src/ui/dist/assets/{NotebookEditor-C-4Kt1p9.js → NotebookEditor-BEQhaQbt.js} +1 -1
- package/src/ui/dist/assets/{NotebookEditor-CVsj8h_T.js → NotebookEditor-DB9N_T9q.js} +23 -23
- package/src/ui/dist/assets/{PdfLoader-CASDQmxJ.js → PdfLoader-eWBONbQP.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BFhwoKsY.js → PdfMarkdownPlugin-D22YOZL3.js} +1 -1
- package/src/ui/dist/assets/{PdfViewerPlugin-DcOzU9vd.js → PdfViewerPlugin-c-RK9DLM.js} +3 -3
- package/src/ui/dist/assets/{SearchPlugin-CHj7M58O.js → SearchPlugin-CxF9ytAx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-CB4DYfWO.js → TextViewerPlugin-C5xqeeUH.js} +2 -2
- package/src/ui/dist/assets/{VNCViewer-CjlbyCB3.js → VNCViewer-BoLGLnHz.js} +1 -1
- package/src/ui/dist/assets/{bot-CFkZY-JP.js → bot-DREQOxzP.js} +1 -1
- package/src/ui/dist/assets/{chevron-up-Dq5ofbht.js → chevron-up-C9Qpx4DE.js} +1 -1
- package/src/ui/dist/assets/{code-DLC6G24T.js → code-WlFHE7z_.js} +1 -1
- package/src/ui/dist/assets/{file-content-Dv4LoZec.js → file-content-BZMz3RYp.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-Denq-lC3.js → file-diff-panel-CQhw0jS2.js} +1 -1
- package/src/ui/dist/assets/{file-socket-Cu4Qln7Y.js → file-socket-CfQPKQKj.js} +1 -1
- package/src/ui/dist/assets/{git-commit-horizontal-BUh6G52n.js → git-commit-horizontal-DxZ8DCZh.js} +1 -1
- package/src/ui/dist/assets/{image-B9HUUddG.js → image-Bgl4VIyx.js} +1 -1
- package/src/ui/dist/assets/{index-Cgla8biy.css → index-BpV6lusQ.css} +1 -1
- package/src/ui/dist/assets/{index-Gbl53BNp.js → index-CBNVuWcP.js} +363 -363
- package/src/ui/dist/assets/{index-wQ7RIIRd.js → index-CwNu1aH4.js} +1 -1
- package/src/ui/dist/assets/{index-B2B1sg-M.js → index-DrUnlf6K.js} +1 -1
- package/src/ui/dist/assets/{index-DRyx7vAc.js → index-NW-h8VzN.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-ZtnHFCAi.js → pdf-effect-queue-J8OnM0jE.js} +1 -1
- package/src/ui/dist/assets/{popover-DL6h35vr.js → popover-CLc0pPP8.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CsX08Qno.js → project-sync-C9IdzdZW.js} +1 -1
- package/src/ui/dist/assets/{select-DvmXt1yY.js → select-Cs2PmzwL.js} +1 -1
- package/src/ui/dist/assets/{sigma-7jpXazui.js → sigma-ClKcHAXm.js} +1 -1
- package/src/ui/dist/assets/{trash-xA7kFt8i.js → trash-DwpbFr3w.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-DsMwDjOp.js → useCliAccess-NQ8m0Let.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-CwMn-iqb.js → wrap-text-BC-Hltpd.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-R-GWEhzS.js → zoom-out-E_gaeAxL.js} +1 -1
- package/src/ui/dist/index.html +2 -2
|
@@ -5,10 +5,10 @@ from collections import deque
|
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from datetime import UTC, datetime, timedelta
|
|
7
7
|
import hashlib
|
|
8
|
-
import subprocess
|
|
9
8
|
import json
|
|
10
9
|
import mimetypes
|
|
11
10
|
import re
|
|
11
|
+
import shutil
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
14
|
from pathlib import Path, PurePosixPath
|
|
@@ -27,7 +27,7 @@ from ..file_lock import advisory_file_lock
|
|
|
27
27
|
from ..gitops import current_branch, export_git_graph, head_commit, init_repo, list_branch_canvas, list_commit_canvas
|
|
28
28
|
from ..home import repo_root
|
|
29
29
|
from ..registries import BaselineRegistry
|
|
30
|
-
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, read_yaml, resolve_within, run_command, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
30
|
+
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, read_yaml, resolve_within, run_command, run_command_bytes, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
31
31
|
from ..skills import SkillInstaller
|
|
32
32
|
from ..web_search import extract_web_search_payload
|
|
33
33
|
from .layout import (
|
|
@@ -322,6 +322,12 @@ class QuestService:
|
|
|
322
322
|
def _quest_root(self, quest_id: str) -> Path:
|
|
323
323
|
return self.quests_root / quest_id
|
|
324
324
|
|
|
325
|
+
def _require_initialized_quest_root(self, quest_id: str) -> Path:
|
|
326
|
+
quest_root = self._quest_root(quest_id)
|
|
327
|
+
if not quest_root.exists() or not self._quest_yaml_path(quest_root).exists():
|
|
328
|
+
raise FileNotFoundError(f"Unknown quest `{quest_id}`.")
|
|
329
|
+
return quest_root
|
|
330
|
+
|
|
325
331
|
def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
|
|
326
332
|
local_present = False
|
|
327
333
|
external_source: str | None = None
|
|
@@ -2782,6 +2788,86 @@ class QuestService:
|
|
|
2782
2788
|
self._initialize_runtime_files(quest_root)
|
|
2783
2789
|
return self.snapshot(quest_id)
|
|
2784
2790
|
|
|
2791
|
+
def repair_orphaned_quest_scaffold(
|
|
2792
|
+
self,
|
|
2793
|
+
quest_id: str,
|
|
2794
|
+
*,
|
|
2795
|
+
title: str | None = None,
|
|
2796
|
+
goal: str | None = None,
|
|
2797
|
+
runner: str = "codex",
|
|
2798
|
+
) -> dict[str, Any]:
|
|
2799
|
+
quest_root = self._quest_root(quest_id)
|
|
2800
|
+
if not quest_root.exists():
|
|
2801
|
+
raise FileNotFoundError(f"Unknown quest `{quest_id}`.")
|
|
2802
|
+
quest_yaml_path = self._quest_yaml_path(quest_root)
|
|
2803
|
+
if quest_yaml_path.exists():
|
|
2804
|
+
raise FileExistsError(f"Quest `{quest_id}` already has a scaffold.")
|
|
2805
|
+
|
|
2806
|
+
restored_goal = str(goal or f"Recovered quest {quest_id}").strip() or f"Recovered quest {quest_id}"
|
|
2807
|
+
restored_title = str(title or quest_id).strip() or quest_id
|
|
2808
|
+
|
|
2809
|
+
for relative in QUEST_DIRECTORIES:
|
|
2810
|
+
ensure_dir(quest_root / relative)
|
|
2811
|
+
|
|
2812
|
+
write_yaml(
|
|
2813
|
+
quest_yaml_path,
|
|
2814
|
+
initial_quest_yaml(
|
|
2815
|
+
quest_id,
|
|
2816
|
+
restored_goal,
|
|
2817
|
+
quest_root,
|
|
2818
|
+
runner,
|
|
2819
|
+
title=restored_title,
|
|
2820
|
+
),
|
|
2821
|
+
)
|
|
2822
|
+
write_text(
|
|
2823
|
+
quest_root / "brief.md",
|
|
2824
|
+
"\n".join(
|
|
2825
|
+
[
|
|
2826
|
+
"# Quest Brief",
|
|
2827
|
+
"",
|
|
2828
|
+
"## Recovery Note",
|
|
2829
|
+
"",
|
|
2830
|
+
"This quest scaffold was recreated because the core quest files were missing.",
|
|
2831
|
+
"Existing runtime traces under `.ds/` were preserved.",
|
|
2832
|
+
"",
|
|
2833
|
+
"## Goal",
|
|
2834
|
+
"",
|
|
2835
|
+
restored_goal,
|
|
2836
|
+
"",
|
|
2837
|
+
]
|
|
2838
|
+
),
|
|
2839
|
+
)
|
|
2840
|
+
write_text(
|
|
2841
|
+
quest_root / "plan.md",
|
|
2842
|
+
"\n".join(
|
|
2843
|
+
[
|
|
2844
|
+
"# Plan",
|
|
2845
|
+
"",
|
|
2846
|
+
"- [ ] Inspect preserved runtime traces under `.ds/`",
|
|
2847
|
+
"- [ ] Re-establish the baseline context",
|
|
2848
|
+
"- [ ] Recreate any missing durable files or artifacts",
|
|
2849
|
+
"",
|
|
2850
|
+
]
|
|
2851
|
+
),
|
|
2852
|
+
)
|
|
2853
|
+
write_text(
|
|
2854
|
+
quest_root / "status.md",
|
|
2855
|
+
"# Status\n\nRecovered scaffold. Review preserved runtime state before continuing.\n",
|
|
2856
|
+
)
|
|
2857
|
+
write_text(
|
|
2858
|
+
quest_root / "SUMMARY.md",
|
|
2859
|
+
"# Summary\n\nRecovered quest scaffold. Original top-level quest files were missing.\n",
|
|
2860
|
+
)
|
|
2861
|
+
write_text(quest_root / ".gitignore", gitignore())
|
|
2862
|
+
self._write_active_user_requirements(
|
|
2863
|
+
quest_root,
|
|
2864
|
+
latest_requirement=None,
|
|
2865
|
+
)
|
|
2866
|
+
if not (quest_root / ".git").exists():
|
|
2867
|
+
init_repo(quest_root)
|
|
2868
|
+
self._initialize_runtime_files(quest_root)
|
|
2869
|
+
return self.snapshot(quest_id)
|
|
2870
|
+
|
|
2785
2871
|
def list_quests(self) -> list[dict]:
|
|
2786
2872
|
items: list[dict] = []
|
|
2787
2873
|
if not self.quests_root.exists():
|
|
@@ -2879,7 +2965,7 @@ class QuestService:
|
|
|
2879
2965
|
)
|
|
2880
2966
|
|
|
2881
2967
|
def summary_compact(self, quest_id: str) -> dict[str, Any]:
|
|
2882
|
-
quest_root = self.
|
|
2968
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
2883
2969
|
cache_key = f"compact:{self._cache_key_for_path(quest_root)}"
|
|
2884
2970
|
state = self._compact_summary_state(quest_root)
|
|
2885
2971
|
with self._snapshot_cache_lock:
|
|
@@ -3254,7 +3340,7 @@ class QuestService:
|
|
|
3254
3340
|
return self._snapshot(quest_id)
|
|
3255
3341
|
|
|
3256
3342
|
def _snapshot(self, quest_id: str) -> dict:
|
|
3257
|
-
quest_root = self.
|
|
3343
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
3258
3344
|
cache_key = f"snapshot:{self._cache_key_for_path(quest_root)}"
|
|
3259
3345
|
state = self._snapshot_state(quest_root)
|
|
3260
3346
|
with self._snapshot_cache_lock:
|
|
@@ -3824,6 +3910,7 @@ class QuestService:
|
|
|
3824
3910
|
title: str | None = None,
|
|
3825
3911
|
active_anchor: str | None = None,
|
|
3826
3912
|
default_runner: str | None = None,
|
|
3913
|
+
workspace_mode: str | None = None,
|
|
3827
3914
|
) -> dict:
|
|
3828
3915
|
quest_root = self._quest_root(quest_id)
|
|
3829
3916
|
quest_yaml_path = self._quest_yaml_path(quest_root)
|
|
@@ -3832,6 +3919,8 @@ class QuestService:
|
|
|
3832
3919
|
|
|
3833
3920
|
quest_data = self.read_quest_yaml(quest_root)
|
|
3834
3921
|
changed = False
|
|
3922
|
+
research_state_updates: dict[str, Any] = {}
|
|
3923
|
+
runtime_state_updates: dict[str, Any] = {}
|
|
3835
3924
|
|
|
3836
3925
|
if title is not None:
|
|
3837
3926
|
normalized_title = str(title).strip()
|
|
@@ -3869,9 +3958,35 @@ class QuestService:
|
|
|
3869
3958
|
quest_data["default_runner"] = normalized_runner
|
|
3870
3959
|
changed = True
|
|
3871
3960
|
|
|
3961
|
+
if workspace_mode is not None:
|
|
3962
|
+
normalized_workspace_mode = str(workspace_mode).strip().lower()
|
|
3963
|
+
if normalized_workspace_mode not in {"copilot", "autonomous"}:
|
|
3964
|
+
raise ValueError("Unsupported workspace mode. Allowed values: copilot, autonomous.")
|
|
3965
|
+
startup_contract = (
|
|
3966
|
+
dict(quest_data.get("startup_contract") or {})
|
|
3967
|
+
if isinstance(quest_data.get("startup_contract"), dict)
|
|
3968
|
+
else {}
|
|
3969
|
+
)
|
|
3970
|
+
if str(startup_contract.get("workspace_mode") or "").strip().lower() != normalized_workspace_mode:
|
|
3971
|
+
startup_contract["workspace_mode"] = normalized_workspace_mode
|
|
3972
|
+
quest_data["startup_contract"] = startup_contract
|
|
3973
|
+
changed = True
|
|
3974
|
+
if str(self.read_research_state(quest_root).get("workspace_mode") or "").strip().lower() != normalized_workspace_mode:
|
|
3975
|
+
research_state_updates["workspace_mode"] = normalized_workspace_mode
|
|
3976
|
+
runtime_state_updates["continuation_policy"] = (
|
|
3977
|
+
"wait_for_user_or_resume" if normalized_workspace_mode == "copilot" else "auto"
|
|
3978
|
+
)
|
|
3979
|
+
runtime_state_updates["continuation_reason"] = (
|
|
3980
|
+
"copilot_mode" if normalized_workspace_mode == "copilot" else "autonomous_mode"
|
|
3981
|
+
)
|
|
3982
|
+
|
|
3872
3983
|
if changed:
|
|
3873
3984
|
quest_data["updated_at"] = utc_now()
|
|
3874
3985
|
write_yaml(quest_yaml_path, quest_data)
|
|
3986
|
+
if research_state_updates:
|
|
3987
|
+
self.update_research_state(quest_root, **research_state_updates)
|
|
3988
|
+
if runtime_state_updates:
|
|
3989
|
+
self.update_runtime_state(quest_root=quest_root, **runtime_state_updates)
|
|
3875
3990
|
|
|
3876
3991
|
return self.snapshot(quest_id)
|
|
3877
3992
|
|
|
@@ -4308,7 +4423,7 @@ class QuestService:
|
|
|
4308
4423
|
return payload
|
|
4309
4424
|
|
|
4310
4425
|
def list_documents(self, quest_id: str) -> list[dict]:
|
|
4311
|
-
quest_root = self.
|
|
4426
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4312
4427
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4313
4428
|
documents = []
|
|
4314
4429
|
for relative in ("brief.md", "plan.md", "status.md", "SUMMARY.md"):
|
|
@@ -4362,7 +4477,7 @@ class QuestService:
|
|
|
4362
4477
|
if revision:
|
|
4363
4478
|
return self._revision_explorer(quest_id, revision=revision, mode=mode or "ref")
|
|
4364
4479
|
|
|
4365
|
-
quest_root = self.
|
|
4480
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4366
4481
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4367
4482
|
git_status = self._git_status_map(workspace_root)
|
|
4368
4483
|
|
|
@@ -4391,7 +4506,7 @@ class QuestService:
|
|
|
4391
4506
|
def search_files(self, quest_id: str, term: str, limit: int = 50) -> dict[str, Any]:
|
|
4392
4507
|
query = term.strip()
|
|
4393
4508
|
normalized_query = query.casefold()
|
|
4394
|
-
workspace_root = self.active_workspace_root(self.
|
|
4509
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
4395
4510
|
resolved_limit = max(1, min(limit, 200))
|
|
4396
4511
|
if not normalized_query:
|
|
4397
4512
|
return {
|
|
@@ -4486,7 +4601,7 @@ class QuestService:
|
|
|
4486
4601
|
}
|
|
4487
4602
|
|
|
4488
4603
|
def open_document(self, quest_id: str, document_id: str) -> dict:
|
|
4489
|
-
quest_root = self.
|
|
4604
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4490
4605
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4491
4606
|
if document_id.startswith("git::"):
|
|
4492
4607
|
revision, relative = self._parse_git_document_id(document_id)
|
|
@@ -4551,7 +4666,7 @@ class QuestService:
|
|
|
4551
4666
|
}
|
|
4552
4667
|
|
|
4553
4668
|
def resolve_document(self, quest_id: str, document_id: str) -> tuple[Path, bool, str, str]:
|
|
4554
|
-
quest_root = self.
|
|
4669
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4555
4670
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4556
4671
|
resolution_root = self._document_resolution_root(
|
|
4557
4672
|
quest_root=quest_root,
|
|
@@ -4744,6 +4859,291 @@ class QuestService:
|
|
|
4744
4859
|
"saved_at": utc_now(),
|
|
4745
4860
|
}
|
|
4746
4861
|
|
|
4862
|
+
@staticmethod
|
|
4863
|
+
def _normalize_workspace_relative_path(
|
|
4864
|
+
relative: str | None,
|
|
4865
|
+
*,
|
|
4866
|
+
field_name: str,
|
|
4867
|
+
allow_root: bool = True,
|
|
4868
|
+
) -> str | None:
|
|
4869
|
+
if relative is None:
|
|
4870
|
+
if allow_root:
|
|
4871
|
+
return None
|
|
4872
|
+
raise ValueError(f"`{field_name}` is required.")
|
|
4873
|
+
raw = str(relative).strip().replace("\\", "/")
|
|
4874
|
+
if not raw:
|
|
4875
|
+
if allow_root:
|
|
4876
|
+
return None
|
|
4877
|
+
raise ValueError(f"`{field_name}` is required.")
|
|
4878
|
+
normalized = raw.lstrip("/").rstrip("/")
|
|
4879
|
+
if normalized in {"", "."}:
|
|
4880
|
+
if allow_root:
|
|
4881
|
+
return None
|
|
4882
|
+
raise ValueError(f"`{field_name}` must point to a workspace entry.")
|
|
4883
|
+
return normalized
|
|
4884
|
+
|
|
4885
|
+
@staticmethod
|
|
4886
|
+
def _normalize_workspace_entry_name(name: str | None, *, field_name: str) -> str:
|
|
4887
|
+
raw = str(name or "").strip().replace("\\", "/")
|
|
4888
|
+
if not raw:
|
|
4889
|
+
raise ValueError(f"`{field_name}` is required.")
|
|
4890
|
+
if "/" in raw:
|
|
4891
|
+
raise ValueError(f"`{field_name}` must be a single path segment.")
|
|
4892
|
+
candidate = Path(raw).name
|
|
4893
|
+
if candidate != raw or candidate in {"", ".", ".."}:
|
|
4894
|
+
raise ValueError(f"`{field_name}` must be a valid file or folder name.")
|
|
4895
|
+
if candidate == ".git":
|
|
4896
|
+
raise ValueError("`.git` cannot be created or renamed from the explorer.")
|
|
4897
|
+
return candidate
|
|
4898
|
+
|
|
4899
|
+
@staticmethod
|
|
4900
|
+
def _normalize_workspace_path_list(paths: Any, *, field_name: str) -> list[str]:
|
|
4901
|
+
if not isinstance(paths, list) or not paths:
|
|
4902
|
+
raise ValueError(f"`{field_name}` must be a non-empty list.")
|
|
4903
|
+
normalized: list[str] = []
|
|
4904
|
+
seen: set[str] = set()
|
|
4905
|
+
for raw in paths:
|
|
4906
|
+
item = QuestService._normalize_workspace_relative_path(
|
|
4907
|
+
raw,
|
|
4908
|
+
field_name=field_name,
|
|
4909
|
+
allow_root=False,
|
|
4910
|
+
)
|
|
4911
|
+
if not item or item in seen:
|
|
4912
|
+
continue
|
|
4913
|
+
seen.add(item)
|
|
4914
|
+
normalized.append(item)
|
|
4915
|
+
if not normalized:
|
|
4916
|
+
raise ValueError(f"`{field_name}` must include at least one valid path.")
|
|
4917
|
+
return normalized
|
|
4918
|
+
|
|
4919
|
+
@staticmethod
|
|
4920
|
+
def _filter_nested_workspace_paths(paths: list[str]) -> list[str]:
|
|
4921
|
+
kept: list[str] = []
|
|
4922
|
+
for path in paths:
|
|
4923
|
+
if any(path == parent or path.startswith(f"{parent}/") for parent in kept):
|
|
4924
|
+
continue
|
|
4925
|
+
kept.append(path)
|
|
4926
|
+
return kept
|
|
4927
|
+
|
|
4928
|
+
def _workspace_entry_payload(self, workspace_root: Path, path: Path) -> dict:
|
|
4929
|
+
if path.is_dir():
|
|
4930
|
+
return self._directory_node(
|
|
4931
|
+
workspace_root,
|
|
4932
|
+
path=path,
|
|
4933
|
+
children=[],
|
|
4934
|
+
git_status={},
|
|
4935
|
+
changed_paths={},
|
|
4936
|
+
)
|
|
4937
|
+
payload = self._file_node(
|
|
4938
|
+
workspace_root,
|
|
4939
|
+
path=path,
|
|
4940
|
+
git_status={},
|
|
4941
|
+
changed_paths={},
|
|
4942
|
+
)
|
|
4943
|
+
if payload is None:
|
|
4944
|
+
raise FileNotFoundError(f"Unknown workspace entry `{path}`.")
|
|
4945
|
+
return payload
|
|
4946
|
+
|
|
4947
|
+
def create_workspace_folder(
|
|
4948
|
+
self,
|
|
4949
|
+
quest_id: str,
|
|
4950
|
+
*,
|
|
4951
|
+
name: str | None,
|
|
4952
|
+
parent_path: str | None = None,
|
|
4953
|
+
) -> dict:
|
|
4954
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
4955
|
+
normalized_parent = self._normalize_workspace_relative_path(
|
|
4956
|
+
parent_path,
|
|
4957
|
+
field_name="parent_path",
|
|
4958
|
+
allow_root=True,
|
|
4959
|
+
)
|
|
4960
|
+
folder_name = self._normalize_workspace_entry_name(name, field_name="name")
|
|
4961
|
+
parent = resolve_within(workspace_root, normalized_parent) if normalized_parent else workspace_root
|
|
4962
|
+
if not parent.exists() or not parent.is_dir():
|
|
4963
|
+
raise FileNotFoundError(
|
|
4964
|
+
f"Unknown destination folder `{normalized_parent or '.'}`."
|
|
4965
|
+
)
|
|
4966
|
+
target = resolve_within(parent, folder_name)
|
|
4967
|
+
if target.exists():
|
|
4968
|
+
raise FileExistsError(
|
|
4969
|
+
f"`{target.relative_to(workspace_root).as_posix()}` already exists."
|
|
4970
|
+
)
|
|
4971
|
+
ensure_dir(target)
|
|
4972
|
+
return {
|
|
4973
|
+
"ok": True,
|
|
4974
|
+
"quest_id": quest_id,
|
|
4975
|
+
"parent_path": normalized_parent,
|
|
4976
|
+
"item": self._workspace_entry_payload(workspace_root, target),
|
|
4977
|
+
"saved_at": utc_now(),
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
def upload_workspace_file(
|
|
4981
|
+
self,
|
|
4982
|
+
quest_id: str,
|
|
4983
|
+
*,
|
|
4984
|
+
file_name: str | None,
|
|
4985
|
+
content: bytes,
|
|
4986
|
+
mime_type: str | None = None,
|
|
4987
|
+
parent_path: str | None = None,
|
|
4988
|
+
) -> dict:
|
|
4989
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
4990
|
+
normalized_parent = self._normalize_workspace_relative_path(
|
|
4991
|
+
parent_path,
|
|
4992
|
+
field_name="parent_path",
|
|
4993
|
+
allow_root=True,
|
|
4994
|
+
)
|
|
4995
|
+
safe_name = self._normalize_workspace_entry_name(file_name, field_name="file_name")
|
|
4996
|
+
parent = resolve_within(workspace_root, normalized_parent) if normalized_parent else workspace_root
|
|
4997
|
+
if not parent.exists() or not parent.is_dir():
|
|
4998
|
+
raise FileNotFoundError(
|
|
4999
|
+
f"Unknown destination folder `{normalized_parent or '.'}`."
|
|
5000
|
+
)
|
|
5001
|
+
target = resolve_within(parent, safe_name)
|
|
5002
|
+
if target.exists():
|
|
5003
|
+
raise FileExistsError(
|
|
5004
|
+
f"`{target.relative_to(workspace_root).as_posix()}` already exists."
|
|
5005
|
+
)
|
|
5006
|
+
ensure_dir(target.parent)
|
|
5007
|
+
target.write_bytes(content)
|
|
5008
|
+
payload = self._workspace_entry_payload(workspace_root, target)
|
|
5009
|
+
guessed_mime = mimetypes.guess_type(target.name)[0] or mime_type or "application/octet-stream"
|
|
5010
|
+
payload["mime_type"] = guessed_mime
|
|
5011
|
+
return {
|
|
5012
|
+
"ok": True,
|
|
5013
|
+
"quest_id": quest_id,
|
|
5014
|
+
"parent_path": normalized_parent,
|
|
5015
|
+
"item": payload,
|
|
5016
|
+
"saved_at": utc_now(),
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
def rename_workspace_entry(
|
|
5020
|
+
self,
|
|
5021
|
+
quest_id: str,
|
|
5022
|
+
*,
|
|
5023
|
+
path: str | None,
|
|
5024
|
+
new_name: str | None,
|
|
5025
|
+
) -> dict:
|
|
5026
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5027
|
+
normalized_path = self._normalize_workspace_relative_path(
|
|
5028
|
+
path,
|
|
5029
|
+
field_name="path",
|
|
5030
|
+
allow_root=False,
|
|
5031
|
+
)
|
|
5032
|
+
source = resolve_within(workspace_root, normalized_path)
|
|
5033
|
+
if not source.exists():
|
|
5034
|
+
raise FileNotFoundError(f"Unknown workspace entry `{normalized_path}`.")
|
|
5035
|
+
safe_name = self._normalize_workspace_entry_name(new_name, field_name="new_name")
|
|
5036
|
+
target = resolve_within(source.parent, safe_name)
|
|
5037
|
+
if target.exists() and target != source:
|
|
5038
|
+
raise FileExistsError(
|
|
5039
|
+
f"`{target.relative_to(workspace_root).as_posix()}` already exists."
|
|
5040
|
+
)
|
|
5041
|
+
if target != source:
|
|
5042
|
+
source.rename(target)
|
|
5043
|
+
payload = self._workspace_entry_payload(workspace_root, target)
|
|
5044
|
+
return {
|
|
5045
|
+
"ok": True,
|
|
5046
|
+
"quest_id": quest_id,
|
|
5047
|
+
"previous_path": normalized_path,
|
|
5048
|
+
"item": payload,
|
|
5049
|
+
"saved_at": utc_now(),
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
def move_workspace_entries(
|
|
5053
|
+
self,
|
|
5054
|
+
quest_id: str,
|
|
5055
|
+
*,
|
|
5056
|
+
paths: Any,
|
|
5057
|
+
target_parent_path: str | None = None,
|
|
5058
|
+
) -> dict:
|
|
5059
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5060
|
+
normalized_paths = self._filter_nested_workspace_paths(
|
|
5061
|
+
self._normalize_workspace_path_list(paths, field_name="paths")
|
|
5062
|
+
)
|
|
5063
|
+
normalized_target_parent = self._normalize_workspace_relative_path(
|
|
5064
|
+
target_parent_path,
|
|
5065
|
+
field_name="target_parent_path",
|
|
5066
|
+
allow_root=True,
|
|
5067
|
+
)
|
|
5068
|
+
target_parent = (
|
|
5069
|
+
resolve_within(workspace_root, normalized_target_parent)
|
|
5070
|
+
if normalized_target_parent
|
|
5071
|
+
else workspace_root
|
|
5072
|
+
)
|
|
5073
|
+
if not target_parent.exists() or not target_parent.is_dir():
|
|
5074
|
+
raise FileNotFoundError(
|
|
5075
|
+
f"Unknown destination folder `{normalized_target_parent or '.'}`."
|
|
5076
|
+
)
|
|
5077
|
+
|
|
5078
|
+
moves: list[tuple[str, Path, Path]] = []
|
|
5079
|
+
destination_keys: set[str] = set()
|
|
5080
|
+
target_parent_resolved = target_parent.resolve()
|
|
5081
|
+
for normalized_path in normalized_paths:
|
|
5082
|
+
source = resolve_within(workspace_root, normalized_path)
|
|
5083
|
+
if not source.exists():
|
|
5084
|
+
raise FileNotFoundError(f"Unknown workspace entry `{normalized_path}`.")
|
|
5085
|
+
source_resolved = source.resolve()
|
|
5086
|
+
if source_resolved == target_parent_resolved or source_resolved in target_parent_resolved.parents:
|
|
5087
|
+
raise ValueError(
|
|
5088
|
+
f"`{normalized_path}` cannot be moved into itself or one of its descendants."
|
|
5089
|
+
)
|
|
5090
|
+
destination = resolve_within(target_parent, source.name)
|
|
5091
|
+
if destination.exists() and destination.resolve() != source_resolved:
|
|
5092
|
+
raise FileExistsError(
|
|
5093
|
+
f"`{destination.relative_to(workspace_root).as_posix()}` already exists."
|
|
5094
|
+
)
|
|
5095
|
+
destination_key = str(destination.resolve())
|
|
5096
|
+
if destination_key in destination_keys and destination != source:
|
|
5097
|
+
raise FileExistsError(
|
|
5098
|
+
f"`{destination.relative_to(workspace_root).as_posix()}` would conflict with another moved entry."
|
|
5099
|
+
)
|
|
5100
|
+
destination_keys.add(destination_key)
|
|
5101
|
+
moves.append((normalized_path, source, destination))
|
|
5102
|
+
|
|
5103
|
+
items: list[dict] = []
|
|
5104
|
+
for _normalized_path, source, destination in moves:
|
|
5105
|
+
if destination != source:
|
|
5106
|
+
source.rename(destination)
|
|
5107
|
+
items.append(self._workspace_entry_payload(workspace_root, destination))
|
|
5108
|
+
return {
|
|
5109
|
+
"ok": True,
|
|
5110
|
+
"quest_id": quest_id,
|
|
5111
|
+
"target_parent_path": normalized_target_parent,
|
|
5112
|
+
"items": items,
|
|
5113
|
+
"saved_at": utc_now(),
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
def delete_workspace_entries(
|
|
5117
|
+
self,
|
|
5118
|
+
quest_id: str,
|
|
5119
|
+
*,
|
|
5120
|
+
paths: Any,
|
|
5121
|
+
) -> dict:
|
|
5122
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5123
|
+
normalized_paths = self._filter_nested_workspace_paths(
|
|
5124
|
+
self._normalize_workspace_path_list(paths, field_name="paths")
|
|
5125
|
+
)
|
|
5126
|
+
sources: list[Path] = []
|
|
5127
|
+
items: list[dict] = []
|
|
5128
|
+
for normalized_path in normalized_paths:
|
|
5129
|
+
source = resolve_within(workspace_root, normalized_path)
|
|
5130
|
+
if not source.exists():
|
|
5131
|
+
raise FileNotFoundError(f"Unknown workspace entry `{normalized_path}`.")
|
|
5132
|
+
sources.append(source)
|
|
5133
|
+
items.append(self._workspace_entry_payload(workspace_root, source))
|
|
5134
|
+
|
|
5135
|
+
for source in sorted(sources, key=lambda item: len(item.parts), reverse=True):
|
|
5136
|
+
if source.is_dir():
|
|
5137
|
+
shutil.rmtree(source)
|
|
5138
|
+
else:
|
|
5139
|
+
source.unlink()
|
|
5140
|
+
return {
|
|
5141
|
+
"ok": True,
|
|
5142
|
+
"quest_id": quest_id,
|
|
5143
|
+
"items": items,
|
|
5144
|
+
"saved_at": utc_now(),
|
|
5145
|
+
}
|
|
5146
|
+
|
|
4747
5147
|
def _revision_explorer(self, quest_id: str, *, revision: str, mode: str) -> dict:
|
|
4748
5148
|
quest_root = self._quest_root(quest_id)
|
|
4749
5149
|
if not self._git_revision_exists(quest_root, revision):
|
|
@@ -4936,6 +5336,8 @@ class QuestService:
|
|
|
4936
5336
|
}
|
|
4937
5337
|
|
|
4938
5338
|
def _initialize_runtime_files(self, quest_root: Path) -> None:
|
|
5339
|
+
if not self._quest_yaml_path(quest_root).exists():
|
|
5340
|
+
raise FileNotFoundError(f"Unknown quest `{quest_root.name}`.")
|
|
4939
5341
|
queue_path = self._message_queue_path(quest_root)
|
|
4940
5342
|
if not queue_path.exists():
|
|
4941
5343
|
write_json(queue_path, self._default_message_queue())
|
|
@@ -5835,12 +6237,10 @@ class QuestService:
|
|
|
5835
6237
|
|
|
5836
6238
|
@staticmethod
|
|
5837
6239
|
def _read_git_bytes(quest_root: Path, revision: str, relative: str) -> bytes:
|
|
5838
|
-
result =
|
|
6240
|
+
result = run_command_bytes(
|
|
5839
6241
|
["git", "show", f"{revision}:{relative}"],
|
|
5840
|
-
cwd=
|
|
6242
|
+
cwd=quest_root,
|
|
5841
6243
|
check=False,
|
|
5842
|
-
text=False,
|
|
5843
|
-
capture_output=True,
|
|
5844
6244
|
)
|
|
5845
6245
|
if result.returncode != 0:
|
|
5846
6246
|
raise FileNotFoundError(f"File `{relative}` does not exist at `{revision}`.")
|
|
@@ -11,12 +11,14 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from ..artifact import ArtifactService
|
|
13
13
|
from ..codex_cli_compat import (
|
|
14
|
+
active_provider_metadata_from_home,
|
|
14
15
|
materialize_codex_runtime_home,
|
|
15
16
|
normalize_codex_reasoning_effort,
|
|
16
17
|
provider_profile_metadata_from_home,
|
|
17
18
|
)
|
|
18
19
|
from ..config import ConfigManager
|
|
19
20
|
from ..gitops import export_git_graph
|
|
21
|
+
from ..process_control import process_session_popen_kwargs
|
|
20
22
|
from ..prompts import PromptBuilder
|
|
21
23
|
from ..runtime_logs import JsonlLogger
|
|
22
24
|
from ..shared import append_jsonl, ensure_dir, generate_id, read_yaml, resolve_runner_binary, utc_now, write_json, write_text
|
|
@@ -76,6 +78,7 @@ _PROVIDER_ENV_CONFLICT_KEYS = (
|
|
|
76
78
|
"OPENAI_API_KEY",
|
|
77
79
|
"OPENAI_BASE_URL",
|
|
78
80
|
)
|
|
81
|
+
_CHAT_WIRE_TOOL_CALL_GUARD_MARKER = "## Codex Chat-Wire Tool Call Compatibility"
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
def _compact_text(value: object, *, limit: int = 1200) -> str:
|
|
@@ -731,6 +734,18 @@ class CodexRunner:
|
|
|
731
734
|
self._process_lock = threading.Lock()
|
|
732
735
|
self._active_processes: dict[str, subprocess.Popen[str]] = {}
|
|
733
736
|
|
|
737
|
+
@staticmethod
|
|
738
|
+
def _subprocess_popen_kwargs(*, workspace_root: Path, env: dict[str, str]) -> dict[str, Any]:
|
|
739
|
+
return {
|
|
740
|
+
"cwd": str(workspace_root),
|
|
741
|
+
"env": env,
|
|
742
|
+
"stdin": subprocess.PIPE,
|
|
743
|
+
"stdout": subprocess.PIPE,
|
|
744
|
+
"stderr": subprocess.PIPE,
|
|
745
|
+
"text": True,
|
|
746
|
+
**process_session_popen_kwargs(hide_window=True),
|
|
747
|
+
}
|
|
748
|
+
|
|
734
749
|
def run(self, request: RunRequest) -> RunResult:
|
|
735
750
|
workspace_root = request.worktree_root or request.quest_root
|
|
736
751
|
run_root = ensure_dir(request.quest_root / ".ds" / "runs" / request.run_id)
|
|
@@ -746,6 +761,7 @@ class CodexRunner:
|
|
|
746
761
|
turn_mode=request.turn_mode,
|
|
747
762
|
retry_context=request.retry_context,
|
|
748
763
|
)
|
|
764
|
+
prompt = self._apply_chat_wire_tool_call_guard(prompt, runner_config=runner_config)
|
|
749
765
|
write_text(run_root / "prompt.md", prompt)
|
|
750
766
|
|
|
751
767
|
codex_home = self._prepare_project_codex_home(
|
|
@@ -796,18 +812,7 @@ class CodexRunner:
|
|
|
796
812
|
env["DS_CONVERSATION_ID"] = f"quest:{request.quest_id}"
|
|
797
813
|
env["DS_AGENT_ROLE"] = request.skill_id
|
|
798
814
|
env["DS_TEAM_MODE"] = "single"
|
|
799
|
-
popen_kwargs
|
|
800
|
-
"cwd": str(workspace_root),
|
|
801
|
-
"env": env,
|
|
802
|
-
"stdin": subprocess.PIPE,
|
|
803
|
-
"stdout": subprocess.PIPE,
|
|
804
|
-
"stderr": subprocess.PIPE,
|
|
805
|
-
"text": True,
|
|
806
|
-
}
|
|
807
|
-
if os.name == "nt" and hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
|
|
808
|
-
popen_kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP")
|
|
809
|
-
else:
|
|
810
|
-
popen_kwargs["start_new_session"] = True
|
|
815
|
+
popen_kwargs = self._subprocess_popen_kwargs(workspace_root=workspace_root, env=env)
|
|
811
816
|
process = subprocess.Popen(command, **popen_kwargs)
|
|
812
817
|
with self._process_lock:
|
|
813
818
|
self._active_processes[request.quest_id] = process
|
|
@@ -1065,6 +1070,46 @@ class CodexRunner:
|
|
|
1065
1070
|
command.append("-")
|
|
1066
1071
|
return command
|
|
1067
1072
|
|
|
1073
|
+
def _apply_chat_wire_tool_call_guard(
|
|
1074
|
+
self,
|
|
1075
|
+
prompt: str,
|
|
1076
|
+
*,
|
|
1077
|
+
runner_config: dict[str, Any] | None = None,
|
|
1078
|
+
) -> str:
|
|
1079
|
+
prompt_text = str(prompt or "")
|
|
1080
|
+
if not prompt_text or _CHAT_WIRE_TOOL_CALL_GUARD_MARKER in prompt_text:
|
|
1081
|
+
return prompt_text
|
|
1082
|
+
|
|
1083
|
+
resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
|
|
1084
|
+
profile = str(resolved_runner_config.get("profile") or "").strip()
|
|
1085
|
+
if not profile:
|
|
1086
|
+
return prompt_text
|
|
1087
|
+
config_home = str(resolved_runner_config.get("config_dir") or os.environ.get("CODEX_HOME") or "").strip()
|
|
1088
|
+
if not config_home:
|
|
1089
|
+
return prompt_text
|
|
1090
|
+
|
|
1091
|
+
metadata = active_provider_metadata_from_home(config_home, profile=profile or None)
|
|
1092
|
+
wire_api = str(metadata.get("wire_api") or "").strip().lower()
|
|
1093
|
+
if wire_api != "chat":
|
|
1094
|
+
return prompt_text
|
|
1095
|
+
|
|
1096
|
+
provider = str(metadata.get("provider") or "").strip() or "unknown"
|
|
1097
|
+
guard_lines = [
|
|
1098
|
+
_CHAT_WIRE_TOOL_CALL_GUARD_MARKER,
|
|
1099
|
+
f"active_provider_profile: {profile}",
|
|
1100
|
+
f"active_provider_name: {provider}",
|
|
1101
|
+
"active_provider_wire_api: chat",
|
|
1102
|
+
"single_tool_call_per_turn_rule: emit at most one tool call in each assistant message.",
|
|
1103
|
+
"tool_call_serialization_rule: after each tool result, decide whether to make the next tool call or produce the answer.",
|
|
1104
|
+
"no_batched_mcp_rule: never bundle multiple `artifact.*`, `memory.*`, or `bash_exec.*` calls into the same response, even when the reads look independent.",
|
|
1105
|
+
"no_immediate_repeat_rule: if a tool already returned the information needed for the current subtask, do not immediately call that same tool again; move to the next tool or answer.",
|
|
1106
|
+
"state_recovery_preference_rule: on a fresh quest turn, prefer `artifact.get_quest_state`, `artifact.read_quest_documents`, and `memory.list_recent` to recover context before reaching for `bash_exec`.",
|
|
1107
|
+
"bash_exec_after_context_rule: use `bash_exec` only after you know the exact command you need and why the `artifact` / `memory` path is insufficient.",
|
|
1108
|
+
"tool_call_json_rule: every tool call must contain exactly one complete JSON object argument with no trailing characters.",
|
|
1109
|
+
]
|
|
1110
|
+
guard_block = "\n".join(guard_lines)
|
|
1111
|
+
return f"{prompt_text.rstrip()}\n\n{guard_block}\n"
|
|
1112
|
+
|
|
1068
1113
|
def _prepare_project_codex_home(
|
|
1069
1114
|
self,
|
|
1070
1115
|
workspace_root: Path,
|
|
@@ -1202,9 +1247,9 @@ class CodexRunner:
|
|
|
1202
1247
|
resolved_runner_config = runner_config if isinstance(runner_config, dict) else {}
|
|
1203
1248
|
profile = str(resolved_runner_config.get("profile") or "").strip()
|
|
1204
1249
|
config_home = str(resolved_runner_config.get("config_dir") or env.get("CODEX_HOME") or "").strip()
|
|
1205
|
-
if not
|
|
1250
|
+
if not config_home:
|
|
1206
1251
|
return env
|
|
1207
|
-
metadata =
|
|
1252
|
+
metadata = active_provider_metadata_from_home(config_home, profile=profile or None)
|
|
1208
1253
|
requires_openai_auth = metadata.get("requires_openai_auth")
|
|
1209
1254
|
if requires_openai_auth is not False:
|
|
1210
1255
|
return env
|