@researai/deepscientist 1.5.6 → 1.5.7

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 (75) hide show
  1. package/README.md +28 -0
  2. package/bin/ds.js +54 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/src/deepscientist/__init__.py +1 -1
  6. package/src/deepscientist/cli.py +1 -1
  7. package/src/deepscientist/config/models.py +6 -6
  8. package/src/deepscientist/config/service.py +5 -0
  9. package/src/deepscientist/connector_profiles.py +34 -6
  10. package/src/deepscientist/daemon/api/handlers.py +6 -1
  11. package/src/deepscientist/daemon/api/router.py +1 -0
  12. package/src/deepscientist/daemon/app.py +230 -3
  13. package/src/deepscientist/prompts/builder.py +20 -4
  14. package/src/deepscientist/qq_profiles.py +19 -9
  15. package/src/deepscientist/quest/layout.py +1 -0
  16. package/src/deepscientist/runners/codex.py +31 -13
  17. package/src/deepscientist/runners/runtime_overrides.py +46 -0
  18. package/src/deepscientist/skills/installer.py +7 -0
  19. package/src/prompts/system.md +30 -0
  20. package/src/skills/analysis-campaign/SKILL.md +30 -0
  21. package/src/skills/analysis-campaign/references/campaign-checklist-template.md +41 -0
  22. package/src/skills/analysis-campaign/references/campaign-plan-template.md +68 -0
  23. package/src/skills/baseline/SKILL.md +105 -5
  24. package/src/skills/baseline/references/baseline-checklist-template.md +57 -0
  25. package/src/skills/baseline/references/baseline-plan-template.md +103 -0
  26. package/src/skills/experiment/SKILL.md +30 -0
  27. package/src/skills/experiment/references/main-experiment-checklist-template.md +52 -0
  28. package/src/skills/experiment/references/main-experiment-plan-template.md +77 -0
  29. package/src/tui/package.json +1 -1
  30. package/src/ui/dist/assets/{AiManusChatView-BGLArZRn.js → AiManusChatView-BS3V4ZOk.js} +11 -11
  31. package/src/ui/dist/assets/{AnalysisPlugin-BgDGSigG.js → AnalysisPlugin-DLPXQsmr.js} +1 -1
  32. package/src/ui/dist/assets/{AutoFigurePlugin-B65HD7L4.js → AutoFigurePlugin-C-Fr9knQ.js} +5 -5
  33. package/src/ui/dist/assets/{CliPlugin-CUqgsFHC.js → CliPlugin-Dd8AHzFg.js} +9 -9
  34. package/src/ui/dist/assets/{CodeEditorPlugin-CF5EdvaS.js → CodeEditorPlugin-Dg-RepTl.js} +8 -8
  35. package/src/ui/dist/assets/{CodeViewerPlugin-DEeU063D.js → CodeViewerPlugin-D2J_3nyt.js} +5 -5
  36. package/src/ui/dist/assets/{DocViewerPlugin-Df-FuDlZ.js → DocViewerPlugin-ChRLLKNb.js} +3 -3
  37. package/src/ui/dist/assets/{GitDiffViewerPlugin-RAnNaRxM.js → GitDiffViewerPlugin-DgHfcved.js} +1 -1
  38. package/src/ui/dist/assets/{ImageViewerPlugin-DXJ0ZJGg.js → ImageViewerPlugin-C89GZMBy.js} +5 -5
  39. package/src/ui/dist/assets/{LabCopilotPanel-BlO-sKsj.js → LabCopilotPanel-BUfIwUcb.js} +10 -10
  40. package/src/ui/dist/assets/{LabPlugin-BajPZW5v.js → LabPlugin-zvUmQUMq.js} +1 -1
  41. package/src/ui/dist/assets/{LatexPlugin-F1OEol8D.js → LatexPlugin-C1SSNuWp.js} +7 -7
  42. package/src/ui/dist/assets/{MarkdownViewerPlugin-MhUupqwT.js → MarkdownViewerPlugin-D2Mf5tU5.js} +4 -4
  43. package/src/ui/dist/assets/{MarketplacePlugin-DxhIEsv0.js → MarketplacePlugin-CF4LgiS2.js} +3 -3
  44. package/src/ui/dist/assets/{NotebookEditor-q7TkhewC.js → NotebookEditor-BM7Bgwlv.js} +1 -1
  45. package/src/ui/dist/assets/{PdfLoader-B8ZOTKFc.js → PdfLoader-Bc5qfD-Z.js} +1 -1
  46. package/src/ui/dist/assets/{PdfMarkdownPlugin-xFPvzvWh.js → PdfMarkdownPlugin-sh1-IRcp.js} +3 -3
  47. package/src/ui/dist/assets/{PdfViewerPlugin-EjEcsIB8.js → PdfViewerPlugin-C_a7CpWG.js} +10 -10
  48. package/src/ui/dist/assets/{SearchPlugin-ixY-1lgW.js → SearchPlugin-L4z3HcLf.js} +1 -1
  49. package/src/ui/dist/assets/{Stepper-gYFK2Pgz.js → Stepper-Dk4aQ3fN.js} +1 -1
  50. package/src/ui/dist/assets/{TextViewerPlugin-Cym6pv_n.js → TextViewerPlugin-BsNtlKVo.js} +4 -4
  51. package/src/ui/dist/assets/{VNCViewer-BPmIHcmK.js → VNCViewer-BpeDcZ5_.js} +9 -9
  52. package/src/ui/dist/assets/{bibtex-Btv6Wi7f.js → bibtex-C4QI-bbj.js} +1 -1
  53. package/src/ui/dist/assets/{code-BlG7g85c.js → code-DuMINRsg.js} +1 -1
  54. package/src/ui/dist/assets/{file-content-DBT5OfTZ.js → file-content-C3N-432K.js} +1 -1
  55. package/src/ui/dist/assets/{file-diff-panel-BWXYzqHk.js → file-diff-panel-CffQ4ZMg.js} +1 -1
  56. package/src/ui/dist/assets/{file-socket-wDlx6byM.js → file-socket-CRH59PCO.js} +1 -1
  57. package/src/ui/dist/assets/{file-utils-Ba3nJmH0.js → file-utils-vYGtW2mI.js} +1 -1
  58. package/src/ui/dist/assets/{image-BwtCyguk.js → image-DBVGaooo.js} +1 -1
  59. package/src/ui/dist/assets/{index-Bz5AaWL7.js → index-B1P6hQRJ.js} +166 -32
  60. package/src/ui/dist/assets/{index-B-2scqCJ.js → index-Be0NAmh8.js} +11 -11
  61. package/src/ui/dist/assets/{index-DcqvKzeJ.js → index-BpjYH9Vg.js} +1 -1
  62. package/src/ui/dist/assets/{index-CfRpE209.js → index-DjSFDmgB.js} +2 -2
  63. package/src/ui/dist/assets/{index-DpMZw8aM.css → index-Do9N28uB.css} +1 -1
  64. package/src/ui/dist/assets/{message-square-BnlyWVH0.js → message-square-BsPDBhiY.js} +1 -1
  65. package/src/ui/dist/assets/{monaco-CXe0pAVe.js → monaco-BTkdPojV.js} +1 -1
  66. package/src/ui/dist/assets/{popover-BCHmVhHj.js → popover-cWjCk-vc.js} +1 -1
  67. package/src/ui/dist/assets/{project-sync-Brk6kaOD.js → project-sync-CXn530xb.js} +1 -1
  68. package/src/ui/dist/assets/{sigma-D72eSUep.js → sigma-04Jr12jg.js} +1 -1
  69. package/src/ui/dist/assets/{tooltip-BMWd0dqX.js → tooltip-BdVDl0G5.js} +1 -1
  70. package/src/ui/dist/assets/{trash-BIt_eWIS.js → trash-CB_GlQyC.js} +1 -1
  71. package/src/ui/dist/assets/{useCliAccess-N1hkTRrR.js → useCliAccess-BL932NwS.js} +1 -1
  72. package/src/ui/dist/assets/{useFileDiffOverlay-DPRPv6rv.js → useFileDiffOverlay-B2WK7Tvq.js} +1 -1
  73. package/src/ui/dist/assets/{wrap-text-E5-UheyP.js → wrap-text-YC68g12z.js} +1 -1
  74. package/src/ui/dist/assets/{zoom-out-D4TR-ZZ_.js → zoom-out-C0RJvFiJ.js} +1 -1
  75. package/src/ui/dist/index.html +2 -2
package/README.md CHANGED
@@ -25,6 +25,20 @@ ds
25
25
 
26
26
  DeepScientist starts the local web workspace at `http://127.0.0.1:20999` by default.
27
27
 
28
+ By default, DeepScientist keeps Codex on the standard profile: `approval_policy=on-request` and `sandbox_mode=workspace-write`. Use `--yolo` only when you want explicit full-access execution.
29
+
30
+ Recommended command when you want the current directory as the home and Codex full-access execution:
31
+
32
+ ```bash
33
+ ds --yolo --port 20999 --here
34
+ ```
35
+
36
+ Parameter meanings:
37
+
38
+ - `--yolo`: run Codex in YOLO mode, which sets `approval_policy=never` and `sandbox_mode=danger-full-access`
39
+ - `--port 20999`: bind the local web workspace to port `20999`
40
+ - `--here`: use the current working directory as the DeepScientist home
41
+
28
42
  On first start, `ds` will:
29
43
 
30
44
  - bootstrap a local `uv` runtime manager automatically if your machine does not already have one
@@ -37,6 +51,12 @@ If you want another port:
37
51
  ds --port 21000
38
52
  ```
39
53
 
54
+ If you want YOLO mode on another port:
55
+
56
+ ```bash
57
+ ds --yolo --port 21000
58
+ ```
59
+
40
60
  If you want to bind on all interfaces:
41
61
 
42
62
  ```bash
@@ -60,6 +80,14 @@ ds --here
60
80
 
61
81
  This is equivalent to launching with `ds --home "$PWD"`.
62
82
 
83
+ Useful launch examples:
84
+
85
+ ```bash
86
+ ds --yolo --port 20999 --here
87
+ ds --host 0.0.0.0 --port 21000
88
+ ds --yolo --host 0.0.0.0 --port 21000 --here
89
+ ```
90
+
63
91
  If you want to install the bundled CLI tree into another base path from a source checkout:
64
92
 
65
93
  ```bash
package/bin/ds.js CHANGED
@@ -38,6 +38,15 @@ const UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
38
38
 
39
39
  const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode', '--proxy']);
40
40
 
41
+ function buildCodexOverrideEnv({ yolo = false } = {}) {
42
+ if (!yolo) {
43
+ return {};
44
+ }
45
+ return {
46
+ DEEPSCIENTIST_CODEX_YOLO: '1',
47
+ };
48
+ }
49
+
41
50
  function printLauncherHelp() {
42
51
  console.log(`DeepScientist launcher
43
52
 
@@ -48,6 +57,7 @@ Usage:
48
57
  ds update --yes
49
58
  ds migrate /data/DeepScientist
50
59
  ds --here
60
+ ds --yolo --port 20999 --here
51
61
  ds --here doctor
52
62
  ds --tui
53
63
  ds --both
@@ -73,6 +83,7 @@ Launcher flags:
73
83
  --home <path> Use a custom DeepScientist home
74
84
  --here Use the current working directory as DeepScientist home
75
85
  --proxy <url> Use an outbound HTTP/WS proxy for npm and Python runtime traffic
86
+ --yolo Run Codex in YOLO mode: approval_policy=never and sandbox_mode=danger-full-access
76
87
  --quest-id <id> Open the TUI on one quest directly
77
88
 
78
89
  Update:
@@ -581,13 +592,14 @@ function pythonVersionText(probe) {
581
592
  return version;
582
593
  }
583
594
 
584
- function renderLaunchHints({ home, url, bindUrl, pythonSelection }) {
595
+ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
585
596
  const runtimeRows = [
586
597
  ['Version', packageJson.version],
587
598
  ['Home', truncateMiddle(home)],
588
599
  ['Browser URL', url],
589
600
  ['Bind URL', bindUrl],
590
601
  ['Python', truncateMiddle(pythonVersionText(pythonSelection))],
602
+ ['Codex mode', yolo ? 'YOLO (never + danger-full-access)' : 'Default (on-request + workspace-write)'],
591
603
  ];
592
604
  if (pythonSelection && pythonSelection.sourceLabel) {
593
605
  runtimeRows.push(['Python source', pythonSelection.sourceLabel]);
@@ -598,6 +610,7 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection }) {
598
610
 
599
611
  console.log(colorize('\u001B[1;38;5;39m', 'Quick Flags'));
600
612
  renderKeyValueRows([
613
+ ['ds --yolo --port 20999 --here', 'Start in the current directory with YOLO Codex access'],
601
614
  ['ds --port 21000', 'Change the web port'],
602
615
  ['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
603
616
  ['ds --here', 'Use the current directory as home'],
@@ -622,6 +635,7 @@ function printLaunchCard({
622
635
  daemonOnly,
623
636
  home,
624
637
  pythonSelection,
638
+ yolo,
625
639
  }) {
626
640
  const width = Math.max(72, Math.min(process.stdout.columns || 100, 108));
627
641
  const divider = colorize('\u001B[38;5;245m', '─'.repeat(Math.max(36, width - 6)));
@@ -680,7 +694,7 @@ function printLaunchCard({
680
694
  console.log(centerText('Run ds --stop to stop the managed daemon.', width));
681
695
  console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
682
696
  console.log('');
683
- renderLaunchHints({ home, url, bindUrl, pythonSelection });
697
+ renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo });
684
698
  }
685
699
 
686
700
  function escapeHtml(value) {
@@ -803,7 +817,7 @@ function writeCodexPreflightReport(home, probe) {
803
817
  };
804
818
  }
805
819
 
806
- function readCodexBootstrapState(home, runtimePython) {
820
+ function readCodexBootstrapState(home, runtimePython, envOverrides = {}) {
807
821
  const snippet = [
808
822
  'import json, pathlib, sys',
809
823
  'from deepscientist.config import ConfigManager',
@@ -811,7 +825,14 @@ function readCodexBootstrapState(home, runtimePython) {
811
825
  'manager = ConfigManager(home)',
812
826
  'print(json.dumps(manager.codex_bootstrap_state(), ensure_ascii=False))',
813
827
  ].join('\n');
814
- const result = runSync(runtimePython, ['-c', snippet, home], { capture: true, allowFailure: true });
828
+ const result = runSync(runtimePython, ['-c', snippet, home], {
829
+ capture: true,
830
+ allowFailure: true,
831
+ env: {
832
+ ...process.env,
833
+ ...envOverrides,
834
+ },
835
+ });
815
836
  if (result.status !== 0) {
816
837
  return { codex_ready: false, codex_last_checked_at: null, codex_last_result: {} };
817
838
  }
@@ -822,7 +843,7 @@ function readCodexBootstrapState(home, runtimePython) {
822
843
  }
823
844
  }
824
845
 
825
- function probeCodexBootstrap(home, runtimePython) {
846
+ function probeCodexBootstrap(home, runtimePython, envOverrides = {}) {
826
847
  const snippet = [
827
848
  'import json, pathlib, sys',
828
849
  'from deepscientist.config import ConfigManager',
@@ -830,7 +851,14 @@ function probeCodexBootstrap(home, runtimePython) {
830
851
  'manager = ConfigManager(home)',
831
852
  'print(json.dumps(manager.probe_codex_bootstrap(persist=True), ensure_ascii=False))',
832
853
  ].join('\n');
833
- const result = runSync(runtimePython, ['-c', snippet, home], { capture: true, allowFailure: true });
854
+ const result = runSync(runtimePython, ['-c', snippet, home], {
855
+ capture: true,
856
+ allowFailure: true,
857
+ env: {
858
+ ...process.env,
859
+ ...envOverrides,
860
+ },
861
+ });
834
862
  let payload = null;
835
863
  try {
836
864
  payload = JSON.parse(result.stdout || '{}');
@@ -881,6 +909,7 @@ function parseLauncherArgs(argv) {
881
909
  let status = false;
882
910
  let daemonOnly = false;
883
911
  let skipUpdateCheck = false;
912
+ let yolo = false;
884
913
 
885
914
  if (args[0] === 'ui') {
886
915
  args.shift();
@@ -899,6 +928,7 @@ function parseLauncherArgs(argv) {
899
928
  else if (arg === '--open-browser') openBrowser = true;
900
929
  else if (arg === '--daemon-only') daemonOnly = true;
901
930
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
931
+ else if (arg === '--yolo') yolo = true;
902
932
  else if (arg === '--host' && args[index + 1]) host = args[++index];
903
933
  else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
904
934
  else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
@@ -923,6 +953,7 @@ function parseLauncherArgs(argv) {
923
953
  questId,
924
954
  daemonOnly,
925
955
  skipUpdateCheck,
956
+ yolo,
926
957
  };
927
958
  }
928
959
 
@@ -2074,6 +2105,9 @@ function normalizePythonCliArgs(args, home) {
2074
2105
  if (arg === '--here') {
2075
2106
  continue;
2076
2107
  }
2108
+ if (arg === '--yolo') {
2109
+ continue;
2110
+ }
2077
2111
  normalized.push(arg);
2078
2112
  }
2079
2113
  return ['--home', home, ...normalized];
@@ -2856,7 +2890,7 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
2856
2890
  }
2857
2891
  }
2858
2892
 
2859
- async function startDaemon(home, runtimePython, host, port, proxy = null) {
2893
+ async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}) {
2860
2894
  const browserUrl = browserUiUrl(host, port);
2861
2895
  const daemonBindUrl = bindUiUrl(host, port);
2862
2896
  const state = readDaemonState(home);
@@ -2880,10 +2914,10 @@ async function startDaemon(home, runtimePython, host, port, proxy = null) {
2880
2914
  removeDaemonState(home);
2881
2915
  }
2882
2916
 
2883
- const bootstrapState = readCodexBootstrapState(home, runtimePython);
2917
+ const bootstrapState = readCodexBootstrapState(home, runtimePython, envOverrides);
2884
2918
  if (!bootstrapState.codex_ready) {
2885
2919
  console.log('Codex is not marked ready yet. Running startup probe...');
2886
- const probe = probeCodexBootstrap(home, runtimePython);
2920
+ const probe = probeCodexBootstrap(home, runtimePython, envOverrides);
2887
2921
  if (!probe || probe.ok !== true) {
2888
2922
  throw createCodexPreflightError(home, probe);
2889
2923
  }
@@ -2915,6 +2949,7 @@ async function startDaemon(home, runtimePython, host, port, proxy = null) {
2915
2949
  stdio: ['ignore', out, out],
2916
2950
  env: {
2917
2951
  ...process.env,
2952
+ ...envOverrides,
2918
2953
  DEEPSCIENTIST_REPO_ROOT: repoRoot,
2919
2954
  DEEPSCIENTIST_NODE_BINARY: process.execPath,
2920
2955
  DEEPSCIENTIST_LAUNCHER_PATH: path.join(repoRoot, 'bin', 'ds.js'),
@@ -3309,6 +3344,7 @@ async function launcherMain(rawArgs) {
3309
3344
 
3310
3345
  const pythonRuntime = ensurePythonRuntime(home);
3311
3346
  const runtimePython = pythonRuntime.runtimePython;
3347
+ const codexOverrideEnv = buildCodexOverrideEnv({ yolo: options.yolo });
3312
3348
  ensureInitialized(home, runtimePython);
3313
3349
  if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
3314
3350
  return true;
@@ -3331,7 +3367,7 @@ async function launcherMain(rawArgs) {
3331
3367
  step(4, 4, 'Starting local daemon and UI surfaces');
3332
3368
  let started;
3333
3369
  try {
3334
- started = await startDaemon(home, runtimePython, host, port, options.proxy);
3370
+ started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv);
3335
3371
  } catch (error) {
3336
3372
  if (handleCodexPreflightFailure(error)) return true;
3337
3373
  throw error;
@@ -3346,6 +3382,7 @@ async function launcherMain(rawArgs) {
3346
3382
  daemonOnly: options.daemonOnly,
3347
3383
  home,
3348
3384
  pythonSelection: pythonRuntime.runtimeProbe,
3385
+ yolo: options.yolo,
3349
3386
  });
3350
3387
 
3351
3388
  if (options.daemonOnly) {
@@ -3381,14 +3418,15 @@ async function main() {
3381
3418
  const home = resolveHome(args);
3382
3419
  const pythonRuntime = ensurePythonRuntime(home);
3383
3420
  const runtimePython = pythonRuntime.runtimePython;
3421
+ const codexOverrideEnv = buildCodexOverrideEnv({ yolo: args.includes('--yolo') });
3384
3422
  if (positional.value === 'run' || positional.value === 'daemon') {
3385
3423
  maybePrintOptionalLatexNotice(home);
3386
3424
  }
3387
3425
  if (positional.value === 'run' || positional.value === 'daemon') {
3388
- const bootstrapState = readCodexBootstrapState(home, runtimePython);
3426
+ const bootstrapState = readCodexBootstrapState(home, runtimePython, codexOverrideEnv);
3389
3427
  if (!bootstrapState.codex_ready) {
3390
3428
  try {
3391
- const probe = probeCodexBootstrap(home, runtimePython);
3429
+ const probe = probeCodexBootstrap(home, runtimePython, codexOverrideEnv);
3392
3430
  if (!probe || probe.ok !== true) {
3393
3431
  throw createCodexPreflightError(home, probe);
3394
3432
  }
@@ -3398,7 +3436,10 @@ async function main() {
3398
3436
  }
3399
3437
  }
3400
3438
  }
3401
- const result = runPythonCli(runtimePython, normalizePythonCliArgs(args, home), { allowFailure: true });
3439
+ const result = runPythonCli(runtimePython, normalizePythonCliArgs(args, home), {
3440
+ allowFailure: true,
3441
+ env: codexOverrideEnv,
3442
+ });
3402
3443
  process.exit(result.status ?? 0);
3403
3444
  return;
3404
3445
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@researai/deepscientist",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "Local-first research operating system with a Python runtime and npm launcher",
5
5
  "license": "MIT",
6
6
  "files": [
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepscientist"
7
- version = "1.5.6"
7
+ version = "1.5.7"
8
8
  description = "DeepScientist Core skeleton"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -5,4 +5,4 @@ __all__ = ["__version__"]
5
5
  try:
6
6
  __version__ = _package_version("deepscientist")
7
7
  except PackageNotFoundError: # pragma: no cover - source checkout fallback
8
- __version__ = "1.5.6"
8
+ __version__ = "1.5.7"
@@ -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"))
@@ -128,7 +128,7 @@ def default_connectors() -> dict:
128
128
  "profiles": [],
129
129
  "app_id": None,
130
130
  "app_secret": None,
131
- "app_secret_env": "QQ_APP_SECRET",
131
+ "app_secret_env": None,
132
132
  "bot_name": "DeepScientist",
133
133
  "command_prefix": "/",
134
134
  "main_chat_id": None,
@@ -149,7 +149,7 @@ def default_connectors() -> dict:
149
149
  "bot_name": "DeepScientist",
150
150
  "command_prefix": "/",
151
151
  "bot_token": None,
152
- "bot_token_env": "TELEGRAM_BOT_TOKEN",
152
+ "bot_token_env": None,
153
153
  "dm_policy": "pairing",
154
154
  "allow_from": [],
155
155
  "group_policy": "open",
@@ -165,7 +165,7 @@ def default_connectors() -> dict:
165
165
  "bot_name": "DeepScientist",
166
166
  "command_prefix": "/",
167
167
  "bot_token": None,
168
- "bot_token_env": "DISCORD_BOT_TOKEN",
168
+ "bot_token_env": None,
169
169
  "application_id": None,
170
170
  "dm_policy": "pairing",
171
171
  "allow_from": [],
@@ -183,10 +183,10 @@ def default_connectors() -> dict:
183
183
  "bot_name": "DeepScientist",
184
184
  "command_prefix": "/",
185
185
  "bot_token": None,
186
- "bot_token_env": "SLACK_BOT_TOKEN",
186
+ "bot_token_env": None,
187
187
  "bot_user_id": None,
188
188
  "app_token": None,
189
- "app_token_env": "SLACK_APP_TOKEN",
189
+ "app_token_env": None,
190
190
  "dm_policy": "pairing",
191
191
  "allow_from": [],
192
192
  "group_policy": "open",
@@ -203,7 +203,7 @@ def default_connectors() -> dict:
203
203
  "command_prefix": "/",
204
204
  "app_id": None,
205
205
  "app_secret": None,
206
- "app_secret_env": "FEISHU_APP_SECRET",
206
+ "app_secret_env": None,
207
207
  "api_base_url": "https://open.feishu.cn",
208
208
  "dm_policy": "pairing",
209
209
  "allow_from": [],
@@ -30,6 +30,7 @@ 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,
@@ -91,6 +92,9 @@ class ConfigManager:
91
92
  def load_named_normalized(self, name: str, create_optional: bool = False) -> dict:
92
93
  return self._normalize_named_payload(name, self.load_named(name, create_optional=create_optional))
93
94
 
95
+ def load_runners_config(self) -> dict:
96
+ return apply_runners_runtime_overrides(self.load_named_normalized("runners"))
97
+
94
98
  def load_named_text(self, name: str, create_optional: bool = False) -> str:
95
99
  path = self.path_for(name)
96
100
  if create_optional and name in OPTIONAL_CONFIG_NAMES and not path.exists():
@@ -1039,6 +1043,7 @@ Use **Test** when the file exposes runtime dependencies.
1039
1043
  )
1040
1044
 
1041
1045
  def _probe_codex_runner(self, config: dict) -> dict:
1046
+ config = apply_codex_runtime_overrides(config)
1042
1047
  checked_at = utc_now()
1043
1048
  binary = str(config.get("binary") or "codex").strip() or "codex"
1044
1049
  resolved_binary = resolve_runner_binary(binary, runner_name="codex")
@@ -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
 
@@ -220,6 +220,9 @@ npm --prefix src/ui run build</pre>
220
220
  def qq_inbound(self, body: dict) -> dict:
221
221
  return self.app.handle_qq_inbound(body)
222
222
 
223
+ def connector_profile_delete(self, connector: str, profile_id: str) -> dict | tuple[int, dict]:
224
+ return self.app.delete_connector_profile(connector, profile_id)
225
+
223
226
  def connector_inbound(self, connector: str, body: dict) -> dict:
224
227
  return self.app.handle_connector_inbound(connector, body)
225
228
 
@@ -1188,7 +1191,7 @@ npm --prefix src/ui run build</pre>
1188
1191
  def run_create(self, quest_id: str, body: dict) -> dict:
1189
1192
  quest_root = self.app.quest_service._quest_root(quest_id)
1190
1193
  config = self.app.config_manager.load_named("config")
1191
- runners = self.app.config_manager.load_named("runners")
1194
+ runners = self.app.config_manager.load_runners_config()
1192
1195
  snapshot = self.app.quest_service.snapshot(quest_id)
1193
1196
  runner_name = str(body.get("runner") or snapshot.get("runner") or config.get("default_runner", "codex")).strip().lower()
1194
1197
  runner_cfg = runners.get(runner_name, {})
@@ -1354,6 +1357,8 @@ npm --prefix src/ui run build</pre>
1354
1357
  result = self.app.config_manager.save_named_text(name, body.get("content", ""))
1355
1358
  if result.get("ok") and name == "connectors":
1356
1359
  result["runtime_reload"] = self.app.reload_connectors_config()
1360
+ if result.get("ok") and name == "runners":
1361
+ result["runtime_reload"] = self.app.reload_runners_config()
1357
1362
  return result
1358
1363
 
1359
1364
  def config_validate(self, body: dict | None = None) -> dict:
@@ -17,6 +17,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
17
17
  ("GET", re.compile(r"^/api/connectors/availability$"), "connectors_availability"),
18
18
  ("GET", re.compile(r"^/api/connectors/qq/bindings$"), "qq_bindings"),
19
19
  ("POST", re.compile(r"^/api/connectors/qq/inbound$"), "qq_inbound"),
20
+ ("DELETE", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/profiles/(?P<profile_id>[^/]+)$"), "connector_profile_delete"),
20
21
  ("GET", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/bindings$"), "connector_bindings"),
21
22
  ("POST", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/inbound$"), "connector_inbound"),
22
23
  ("GET", re.compile(r"^/api/baselines$"), "baselines"),