@team-agent/installer 0.4.6 → 0.4.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.
@@ -6,7 +6,7 @@ use std::process::Command;
6
6
  use super::helpers::{find_session_id, parse_jsonl_records, patterns};
7
7
  use super::types::{
8
8
  AuthHintStatus, CaptureVia, CapturedSession, CommandPlan, Confidence, McpConfig,
9
- ProviderCaps, ProviderCommandContext, ProviderCommandOverrides, ProviderError, RolloutPath,
9
+ ProviderCaps, ProviderCommandContext, ProviderError, RolloutPath,
10
10
  SessionId, StatusPatterns,
11
11
  };
12
12
  use super::{AuthMode, Provider};
@@ -296,7 +296,7 @@ pub fn get_adapter(p: Provider) -> Box<dyn ProviderAdapter> {
296
296
  }
297
297
 
298
298
  #[derive(Debug, Clone, Copy)]
299
- struct BasicProviderAdapter {
299
+ pub(crate) struct BasicProviderAdapter {
300
300
  provider: Provider,
301
301
  }
302
302
 
@@ -928,26 +928,7 @@ impl ProviderAdapter for BasicProviderAdapter {
928
928
  }
929
929
  }
930
930
 
931
- fn command_name(provider: Provider) -> &'static str {
932
- match provider {
933
- Provider::Claude | Provider::ClaudeCode => "claude",
934
- Provider::Codex => "codex",
935
- Provider::Copilot => "copilot",
936
- Provider::GeminiCli => "gemini",
937
- Provider::Fake => "team-agent",
938
- }
939
- }
940
-
941
- fn provider_wire(provider: Provider) -> &'static str {
942
- match provider {
943
- Provider::Claude => "claude",
944
- Provider::ClaudeCode => "claude_code",
945
- Provider::Codex => "codex",
946
- Provider::Copilot => "copilot",
947
- Provider::GeminiCli => "gemini_cli",
948
- Provider::Fake => "fake",
949
- }
950
- }
931
+ use crate::provider::wire::{command_name, provider_wire};
951
932
 
952
933
  fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
953
934
  match auth_mode {
@@ -1559,364 +1540,29 @@ fn path_is_under_team_runtime(path: &Path) -> bool {
1559
1540
  .any(|c| c.as_os_str() == std::ffi::OsStr::new(".team"))
1560
1541
  }
1561
1542
 
1562
- fn fake_worker_command() -> Vec<String> {
1563
- let exe = std::env::current_exe()
1564
- .ok()
1565
- .and_then(|p| p.into_os_string().into_string().ok())
1566
- .unwrap_or_else(|| "team-agent".to_string());
1567
- vec![
1568
- exe,
1569
- "fake-worker".to_string(),
1570
- "--workspace".to_string(),
1571
- "{workspace}".to_string(),
1572
- "--agent-id".to_string(),
1573
- "{agent_id}".to_string(),
1574
- ]
1575
- }
1576
-
1577
- fn claude_launch_command(
1578
- adapter: &BasicProviderAdapter,
1579
- auth_mode: AuthMode,
1580
- mcp_config: Option<&McpConfig>,
1581
- system_prompt: Option<&str>,
1582
- model: Option<&str>,
1583
- tools: &[&str],
1584
- ) -> Result<Vec<String>, ProviderError> {
1585
- let mut argv = claude_base_command(adapter, auth_mode, mcp_config, system_prompt, model, tools, false)?;
1586
- argv.push("--session-id".to_string());
1587
- argv.push(next_session_token());
1588
- Ok(argv)
1589
- }
1590
-
1591
- fn claude_base_command(
1592
- adapter: &BasicProviderAdapter,
1593
- auth_mode: AuthMode,
1594
- mcp_config: Option<&McpConfig>,
1595
- system_prompt: Option<&str>,
1596
- model: Option<&str>,
1597
- tools: &[&str],
1598
- managed_mcp_config: bool,
1599
- ) -> Result<Vec<String>, ProviderError> {
1600
- let mut argv = vec!["claude".to_string()];
1601
- if claude_dangerous_auto_approve(tools) {
1602
- argv.push("--dangerously-skip-permissions".to_string());
1603
- } else {
1604
- argv.push("--permission-mode".to_string());
1605
- argv.push("default".to_string());
1606
- }
1607
- if let Some(model) = model {
1608
- argv.push("--model".to_string());
1609
- argv.push(model.to_string());
1610
- }
1611
- if let Some(prompt) = system_prompt {
1612
- argv.push("--append-system-prompt".to_string());
1613
- argv.push(prompt.to_string());
1614
- }
1615
- if !managed_mcp_config
1616
- && (mcp_config.is_some()
1617
- || auth_mode == AuthMode::CompatibleApi
1618
- || system_prompt.is_some_and(prompt_needs_native_mcp))
1619
- {
1620
- let raw = if let Some(config) = mcp_config {
1621
- serde_json::json!({"mcpServers": config.raw.clone()})
1622
- } else {
1623
- serde_json::json!({"mcpServers": adapter.mcp_config(auth_mode)?.raw})
1624
- };
1625
- argv.push("--mcp-config".to_string());
1626
- argv.push(raw.to_string());
1627
- }
1628
- for tool in claude_disallowed_tools(tools) {
1629
- argv.push("--disallowedTools".to_string());
1630
- argv.push(tool.to_string());
1631
- }
1632
- Ok(argv)
1633
- }
1634
-
1635
- fn prompt_needs_native_mcp(prompt: &str) -> bool {
1543
+ pub(crate) fn prompt_needs_native_mcp(prompt: &str) -> bool {
1636
1544
  prompt.contains('\n') || prompt.contains('"')
1637
1545
  }
1638
1546
 
1639
- fn codex_base_command(
1640
- subcommand: Option<&str>,
1641
- _auth_mode: AuthMode,
1642
- mcp_config: Option<&McpConfig>,
1643
- system_prompt: Option<&str>,
1644
- model: Option<&str>,
1645
- tools: &[&str],
1646
- overrides: Option<&ProviderCommandOverrides>,
1647
- ) -> Vec<String> {
1648
- let mut argv = vec![
1649
- "codex".to_string(),
1650
- ];
1651
- if let Some(subcommand) = subcommand {
1652
- argv.push(subcommand.to_string());
1653
- }
1654
- argv.extend([
1655
- "--no-alt-screen".to_string(),
1656
- "--disable".to_string(),
1657
- "shell_snapshot".to_string(),
1658
- "--disable".to_string(),
1659
- "apps".to_string(),
1660
- ]);
1661
- // codex.py:105-107 — profile CODEX_PROFILE before the sandbox/approval flags.
1662
- if let Some(profile) = overrides.and_then(|o| o.codex_profile.as_deref()) {
1663
- argv.push("--profile".to_string());
1664
- argv.push(profile.to_string());
1665
- }
1666
- if codex_dangerous_auto_approve(tools) {
1667
- argv.push("--dangerously-bypass-approvals-and-sandbox".to_string());
1668
- } else {
1669
- argv.push("--sandbox".to_string());
1670
- argv.push(codex_sandbox_mode(tools).to_string());
1671
- argv.push("--ask-for-approval".to_string());
1672
- argv.push("on-request".to_string());
1673
- }
1674
- if let Some(model) = model {
1675
- argv.push("--model".to_string());
1676
- argv.push(model.to_string());
1677
- }
1678
- // codex.py:117-118 — profile codex_config `-c` items before developer_instructions.
1679
- if let Some(overrides) = overrides {
1680
- for config in &overrides.codex_config {
1681
- argv.push("-c".to_string());
1682
- argv.push(config.clone());
1683
- }
1684
- }
1685
- if let Some(prompt) = system_prompt {
1686
- // codex.py:120 — escape order matters: backslash first, then quote, then newline.
1687
- let escaped = prompt
1688
- .replace('\\', "\\\\")
1689
- .replace('"', "\\\"")
1690
- .replace('\n', "\\n");
1691
- argv.push("-c".to_string());
1692
- argv.push(format!("developer_instructions=\"{escaped}\""));
1693
- }
1694
- // Contract C / MUST-8: Codex CLI (2026-06) does NOT take Claude's `--mcp-config <json>` flag;
1695
- // instead it uses `-c mcp_servers.<name>.<field>=...` overrides, the same pattern used by
1696
- // the live Team Agent workers in this very session (attach-leader codex panes spawn with
1697
- // `-c mcp_servers.team_orchestrator.command="..."`, ` ...args=[...]`, `...env.TEAM_AGENT_ID=...`).
1698
- // Inject the resolved MCP config that way so the Codex worker has a real callable
1699
- // `team_orchestrator` server (not prompt-only metadata).
1700
- if let Some(config) = mcp_config {
1701
- append_codex_mcp_overrides(&mut argv, &config.raw);
1702
- }
1703
- argv
1704
- }
1705
-
1706
- /// Render an `McpConfig::raw` ({ name: { type, command, args, env: {...} } }) into Codex
1707
- /// `-c mcp_servers.<name>.<field>=...` overrides. JSON values are stringified with serde
1708
- /// so arrays/objects survive (Codex parses the right-hand side as JSON; this is what the
1709
- /// Python golden + the live attached Codex panes do).
1710
- fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_json::Value) {
1711
- let Some(servers) = raw.as_object() else {
1712
- return;
1713
- };
1714
- for (name, server) in servers {
1715
- let Some(obj) = server.as_object() else {
1716
- continue;
1717
- };
1718
- for (key, value) in obj {
1719
- if key == "env" {
1720
- if let Some(env) = value.as_object() {
1721
- for (env_key, env_value) in env {
1722
- argv.push("-c".to_string());
1723
- argv.push(format!(
1724
- "mcp_servers.{name}.env.{env_key}={}",
1725
- json_inline(env_value)
1726
- ));
1727
- }
1728
- }
1729
- continue;
1730
- }
1731
- argv.push("-c".to_string());
1732
- argv.push(format!("mcp_servers.{name}.{key}={}", json_inline(value)));
1733
- }
1734
- // codex.py:129 — every MCP server gets a 600s tool timeout so long-running
1735
- // team_orchestrator calls (report_result etc.) survive the codex default.
1736
- argv.push("-c".to_string());
1737
- argv.push(format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
1738
- }
1739
- }
1740
-
1741
- fn json_inline(value: &serde_json::Value) -> String {
1547
+ /// Shared JSON renderer used by the codex `-c mcp_servers.*=...` overrides
1548
+ /// and any other adapter that wants inline JSON. String values are quoted
1549
+ /// (with quote escape); other values use serde's default Display.
1550
+ pub(crate) fn json_inline(value: &serde_json::Value) -> String {
1742
1551
  match value {
1743
1552
  serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
1744
1553
  other => other.to_string(),
1745
1554
  }
1746
1555
  }
1747
1556
 
1748
- fn codex_dangerous_auto_approve(tools: &[&str]) -> bool {
1749
- tools.contains(&"dangerous_auto_approve")
1750
- }
1751
-
1752
- // ---------------------------------------------------------------------------
1753
- // COPILOT base command(v2 实证 + cr verdict v2 30 约束)
1754
- // ---------------------------------------------------------------------------
1755
- //
1756
- // 设计 v2 §B argv 形态(每条带 help 出处,逐字落地):
1757
- // copilot --no-color --no-auto-update --no-remote # C-1-2 噪音 + 禁远控
1758
- // --disable-builtin-mcps # C-3-1 P0 禁内建 github-mcp-server
1759
- // --additional-mcp-config @<file> # C-3-4 用 @file 形,避 wrapper 语义
1760
- // --allow-tool 'team_orchestrator' # C-3-5 mcp_team 免审批 (server 级)
1761
- // --session-id <uuid> -n <agent_id> # C-7-1 plan/launch 加
1762
- // -C <workspace> # 双保险,launch 加
1763
- // [--allow-all | <granular deny>] # C-5-1/C-5-2
1764
- // [--model <m>]
1765
- // [--log-dir <dir> --log-level info] # C-6-2 launch 加
1766
- // env: COPILOT_CUSTOM_INSTRUCTIONS_DIRS=<ws>/.../<agent_id>/ # C-2-1 launch 注入
1767
- // COPILOT_DISABLE_TERMINAL_TITLE=1 # C-4 P0 N39 红线,launch 注入
1768
- // banner 不入 argv(v1 错的 --banner=never 删除;banner 走 config 文件,非 CLI flag)。
1769
- // `-i`/`-p`/`--share*`/`--no-ask-user` **绝不**入 argv(RC-1/RC-14/RC-16)。
1770
- //
1771
- // system_prompt(B2 灵魂件)**不进 argv**:走 spawn env COPILOT_CUSTOM_INSTRUCTIONS_DIRS
1772
- // + per-worker AGENTS.md(B2 单源,不另拼);本函数 system_prompt 参数静默忽略。
1773
- fn copilot_base_command(
1774
- auth_mode: AuthMode,
1775
- mcp_config: Option<&McpConfig>,
1776
- system_prompt: Option<&str>,
1777
- model: Option<&str>,
1778
- tools: &[&str],
1779
- ) -> Vec<String> {
1780
- let _ = (auth_mode, system_prompt);
1781
- let mut argv = vec![
1782
- "copilot".to_string(),
1783
- // C-1-2 v2:噪音控制三件 + 禁远控(防 GitHub web 远控 worker)
1784
- "--no-color".to_string(),
1785
- "--no-auto-update".to_string(),
1786
- "--no-remote".to_string(),
1787
- // C-3-1 v2 (P0):禁内建 github-mcp-server(main-help:70-71);残留风险
1788
- // 通过 spawn 前 `copilot mcp list` 扫描 + 按名 `--disable-mcp-server <n>` 补
1789
- // (那一段在 launch 路径加,因为需要 spawn-time 探测)。
1790
- "--disable-builtin-mcps".to_string(),
1791
- ];
1792
- if copilot_dangerous_auto_approve(tools) {
1793
- // C-5-1 v2 实证:--allow-all == --yolo == 三件套(tools+paths+urls 等价),
1794
- // help-permissions Enabling All Permissions 节原文。**禁** --allow-all-tools
1795
- // (仅 tools 一档,语义不全,RC-13)。
1796
- argv.push("--allow-all".to_string());
1797
- } else {
1798
- // C-5-2 v2:角色缺某 canonical 能力 → 精细 deny(deny 恒优先,即便
1799
- // --allow-all-tools,help-permissions 原文);**禁** --allow-all/--yolo(RC-14)。
1800
- for flag in copilot_permission_flags(tools) {
1801
- argv.push(flag);
1802
- }
1803
- }
1804
- // C-3-5 v2:mcp_team ∈ canonical(team_orchestrator 是我们的 server)→ 免审批
1805
- // (模式 `<mcp-server-name>(tool-name?)` 省略 tool = 该 server 全工具集)。
1806
- argv.push("--allow-tool".to_string());
1807
- argv.push("team_orchestrator".to_string());
1808
- if let Some(model) = model {
1809
- argv.push("--model".to_string());
1810
- argv.push(model.to_string());
1811
- }
1812
- if let Some(config) = mcp_config {
1813
- // §C1 v2 + C-3-4 cr verdict v2(te 真机实证 cmd-mcp-add schema):
1814
- // copilot 的 mcp 配置 schema 字段名是 `transport`(取值 stdio|http|sse),
1815
- // 不是 codex/claude 的 `type`。McpConfig.raw 是 canonical(type),写
1816
- // --additional-mcp-config 时必须翻译 type→transport(仅 copilot 走此分支)。
1817
- argv.push("--additional-mcp-config".to_string());
1818
- argv.push(copilot_translate_mcp_config(&config.raw).to_string());
1819
- }
1820
- argv
1821
- }
1822
-
1823
- /// C-3-4 cr verdict v2 — 把 McpConfig.raw 的 canonical schema(`type`)翻译成
1824
- /// copilot mcp add/--additional-mcp-config 期望的 `transport` 字段(stdio|http|sse)。
1825
- /// 仅 Copilot 适配走此翻译,claude/codex 路径不动。
1826
- fn copilot_translate_mcp_config(raw: &serde_json::Value) -> serde_json::Value {
1827
- let Some(servers) = raw.as_object() else {
1828
- return raw.clone();
1829
- };
1830
- let mut translated = serde_json::Map::new();
1831
- for (name, server) in servers {
1832
- let Some(obj) = server.as_object() else {
1833
- translated.insert(name.clone(), server.clone());
1834
- continue;
1835
- };
1836
- let mut out = serde_json::Map::new();
1837
- for (key, value) in obj {
1838
- if key == "type" {
1839
- out.insert("transport".to_string(), value.clone());
1840
- } else {
1841
- out.insert(key.clone(), value.clone());
1842
- }
1843
- }
1844
- translated.insert(name.clone(), serde_json::Value::Object(out));
1845
- }
1846
- serde_json::Value::Object(translated)
1847
- }
1848
-
1849
- /// resume 路径同 base + `--resume <sid>`(去 --session-id);单列出
1850
- /// 避免 plan 端误 push --session-id 与 --resume 同帧。
1851
- fn copilot_base_command_resume(
1852
- auth_mode: AuthMode,
1853
- mcp_config: Option<&McpConfig>,
1854
- system_prompt: Option<&str>,
1855
- model: Option<&str>,
1856
- tools: &[&str],
1857
- ) -> Vec<String> {
1858
- copilot_base_command(auth_mode, mcp_config, system_prompt, model, tools)
1859
- }
1860
-
1861
- fn copilot_dangerous_auto_approve(tools: &[&str]) -> bool {
1862
- tools.contains(&"dangerous_auto_approve")
1863
- }
1864
-
1865
- /// C-5-2 v2 verdict — copilot 细粒度 deny 映射(canonical tool → copilot flag,
1866
- /// 全部走 `--deny-tool <kind>`,help-permissions Tool Permissions 节四 kind:
1867
- /// shell/write/mcp/url):
1868
- /// execute_bash ∉ allowed → `--deny-tool 'shell'`
1869
- /// fs_write ∉ allowed → `--deny-tool 'write'`
1870
- /// network ∉ allowed → `--deny-tool 'url'`(help-permissions: "url(domain-or-url?)
1871
- /// … If omitted, matches all URLs")
1872
- /// fs_read/fs_list 在 copilot 上无对应 deny kind(C-5-3 prompt_only 诚实)。
1873
- fn copilot_permission_flags(tools: &[&str]) -> Vec<String> {
1874
- let mut flags = Vec::new();
1875
- if !tools.contains(&"execute_bash") {
1876
- flags.push("--deny-tool".to_string());
1877
- flags.push("shell".to_string());
1878
- }
1879
- if !tools.contains(&"fs_write") {
1880
- flags.push("--deny-tool".to_string());
1881
- flags.push("write".to_string());
1882
- }
1883
- if !tools.contains(&"network") {
1884
- // v2 修正:`--deny-tool 'url'`(省略 domain 匹配全 URL),不是 `--deny-url '*'`
1885
- // (RC-19 反向 case 守 — 全 URL 拒绝走 deny-tool kind,不走 deny-url path)。
1886
- flags.push("--deny-tool".to_string());
1887
- flags.push("url".to_string());
1888
- }
1889
- flags
1890
- }
1891
-
1892
- fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
1893
- tools.contains(&"dangerous_auto_approve")
1894
- }
1895
-
1896
- fn claude_disallowed_tools(tools: &[&str]) -> Vec<&'static str> {
1897
- let mut disallowed = Vec::new();
1898
- if !tools.contains(&"execute_bash") {
1899
- disallowed.push("Bash");
1900
- }
1901
- if !tools.contains(&"fs_read") {
1902
- disallowed.push("Read");
1903
- }
1904
- if !tools.contains(&"fs_write") {
1905
- disallowed.extend(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
1906
- }
1907
- if !tools.contains(&"fs_list") {
1908
- disallowed.extend(["Glob", "Grep"]);
1909
- }
1910
- disallowed
1911
- }
1912
-
1913
- fn codex_sandbox_mode(tools: &[&str]) -> &'static str {
1914
- if tools.iter().any(|tool| matches!(*tool, "fs_write" | "execute_bash")) {
1915
- "workspace-write"
1916
- } else {
1917
- "read-only"
1918
- }
1919
- }
1557
+ // 0.4.x decoupling step 2: provider-local command builders moved to provider/adapters/.
1558
+ // Only the entry points the trait impl actually calls are re-imported here;
1559
+ // the per-provider helper fns (dangerous_auto_approve, permission flags,
1560
+ // disallowed_tools, sandbox_mode, mcp_overrides) are called from within the
1561
+ // extracted base_command fns, not directly by this file.
1562
+ use super::adapters::claude::{claude_base_command, claude_launch_command};
1563
+ use super::adapters::codex::codex_base_command;
1564
+ use super::adapters::copilot::{copilot_base_command, copilot_base_command_resume};
1565
+ use super::adapters::fake::fake_worker_command;
1920
1566
 
1921
1567
  /// Contract C / MUST-8: the per-worker Team Agent MCP server config used by Claude
1922
1568
  /// (`--mcp-config`) and Codex (`-c mcp_servers.*` injection). Placeholders
@@ -2010,7 +1656,7 @@ fn has_cwd_field(record: &serde_json::Value) -> bool {
2010
1656
  record_cwd(record).is_some()
2011
1657
  }
2012
1658
 
2013
- fn next_session_token() -> String {
1659
+ pub(crate) fn next_session_token() -> String {
2014
1660
  use sha2::Digest;
2015
1661
 
2016
1662
  static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
@@ -0,0 +1,106 @@
1
+ //! Claude / ClaudeCode provider-local command builders + permission helpers.
2
+ //!
3
+ //! Extracted from `provider/adapter.rs` (0.4.x decoupling step 2). Pure
4
+ //! extraction — byte-identical to the original inline forms. Scope kept
5
+ //! small on purpose: base command + permission/disallowed-tool mapping +
6
+ //! launch wrapper. Auth hints (`claude_auth_hint`), capture-related
7
+ //! helpers (`claude_projects_dir_for_cwd`, `encode_claude_projects_dir`,
8
+ //! `rollout_path_has_claude_leader_marker`,
9
+ //! `claude_records_have_leader_marker`), and the context-aware model
10
+ //! resolver (`claude_context_model`) stay in `adapter.rs` because they
11
+ //! depend on adapter-private utilities (capture scanning, `command_on_path`,
12
+ //! `ProfileLaunchContext`). A later step can move those into a session
13
+ //! store / auth descriptor hook.
14
+
15
+ use crate::model::enums::AuthMode;
16
+ use crate::provider::adapter::{
17
+ next_session_token, prompt_needs_native_mcp, BasicProviderAdapter,
18
+ };
19
+ use crate::provider::{McpConfig, ProviderAdapter, ProviderError};
20
+
21
+ pub(crate) fn claude_launch_command(
22
+ adapter: &BasicProviderAdapter,
23
+ auth_mode: AuthMode,
24
+ mcp_config: Option<&McpConfig>,
25
+ system_prompt: Option<&str>,
26
+ model: Option<&str>,
27
+ tools: &[&str],
28
+ ) -> Result<Vec<String>, ProviderError> {
29
+ let mut argv = claude_base_command(
30
+ adapter,
31
+ auth_mode,
32
+ mcp_config,
33
+ system_prompt,
34
+ model,
35
+ tools,
36
+ false,
37
+ )?;
38
+ argv.push("--session-id".to_string());
39
+ argv.push(next_session_token());
40
+ Ok(argv)
41
+ }
42
+
43
+ pub(crate) fn claude_base_command(
44
+ adapter: &BasicProviderAdapter,
45
+ auth_mode: AuthMode,
46
+ mcp_config: Option<&McpConfig>,
47
+ system_prompt: Option<&str>,
48
+ model: Option<&str>,
49
+ tools: &[&str],
50
+ managed_mcp_config: bool,
51
+ ) -> Result<Vec<String>, ProviderError> {
52
+ let mut argv = vec!["claude".to_string()];
53
+ if claude_dangerous_auto_approve(tools) {
54
+ argv.push("--dangerously-skip-permissions".to_string());
55
+ } else {
56
+ argv.push("--permission-mode".to_string());
57
+ argv.push("default".to_string());
58
+ }
59
+ if let Some(model) = model {
60
+ argv.push("--model".to_string());
61
+ argv.push(model.to_string());
62
+ }
63
+ if let Some(prompt) = system_prompt {
64
+ argv.push("--append-system-prompt".to_string());
65
+ argv.push(prompt.to_string());
66
+ }
67
+ if !managed_mcp_config
68
+ && (mcp_config.is_some()
69
+ || auth_mode == AuthMode::CompatibleApi
70
+ || system_prompt.is_some_and(prompt_needs_native_mcp))
71
+ {
72
+ let raw = if let Some(config) = mcp_config {
73
+ serde_json::json!({"mcpServers": config.raw.clone()})
74
+ } else {
75
+ serde_json::json!({"mcpServers": adapter.mcp_config(auth_mode)?.raw})
76
+ };
77
+ argv.push("--mcp-config".to_string());
78
+ argv.push(raw.to_string());
79
+ }
80
+ for tool in claude_disallowed_tools(tools) {
81
+ argv.push("--disallowedTools".to_string());
82
+ argv.push(tool.to_string());
83
+ }
84
+ Ok(argv)
85
+ }
86
+
87
+ pub(crate) fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
88
+ tools.contains(&"dangerous_auto_approve")
89
+ }
90
+
91
+ pub(crate) fn claude_disallowed_tools(tools: &[&str]) -> Vec<&'static str> {
92
+ let mut disallowed = Vec::new();
93
+ if !tools.contains(&"execute_bash") {
94
+ disallowed.push("Bash");
95
+ }
96
+ if !tools.contains(&"fs_read") {
97
+ disallowed.push("Read");
98
+ }
99
+ if !tools.contains(&"fs_write") {
100
+ disallowed.extend(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
101
+ }
102
+ if !tools.contains(&"fs_list") {
103
+ disallowed.extend(["Glob", "Grep"]);
104
+ }
105
+ disallowed
106
+ }
@@ -0,0 +1,117 @@
1
+ //! Codex provider-local command builder + permission/sandbox helpers.
2
+ //!
3
+ //! Extracted from `provider/adapter.rs` (0.4.x decoupling step 2). Pure
4
+ //! extraction — byte-identical to the original inline forms. Shared helper
5
+ //! `json_inline` stays in `adapter.rs` because it is also used by other
6
+ //! sites; this file reaches it via `super::*`.
7
+
8
+ use crate::model::enums::AuthMode;
9
+ use crate::provider::adapter::json_inline;
10
+ use crate::provider::{McpConfig, ProviderCommandOverrides};
11
+
12
+ pub(crate) fn codex_base_command(
13
+ subcommand: Option<&str>,
14
+ _auth_mode: AuthMode,
15
+ mcp_config: Option<&McpConfig>,
16
+ system_prompt: Option<&str>,
17
+ model: Option<&str>,
18
+ tools: &[&str],
19
+ overrides: Option<&ProviderCommandOverrides>,
20
+ ) -> Vec<String> {
21
+ let mut argv = vec!["codex".to_string()];
22
+ if let Some(subcommand) = subcommand {
23
+ argv.push(subcommand.to_string());
24
+ }
25
+ argv.extend([
26
+ "--no-alt-screen".to_string(),
27
+ "--disable".to_string(),
28
+ "shell_snapshot".to_string(),
29
+ "--disable".to_string(),
30
+ "apps".to_string(),
31
+ ]);
32
+ if let Some(profile) = overrides.and_then(|o| o.codex_profile.as_deref()) {
33
+ argv.push("--profile".to_string());
34
+ argv.push(profile.to_string());
35
+ }
36
+ if codex_dangerous_auto_approve(tools) {
37
+ argv.push("--dangerously-bypass-approvals-and-sandbox".to_string());
38
+ } else {
39
+ argv.push("--sandbox".to_string());
40
+ argv.push(codex_sandbox_mode(tools).to_string());
41
+ argv.push("--ask-for-approval".to_string());
42
+ argv.push("on-request".to_string());
43
+ }
44
+ if let Some(model) = model {
45
+ argv.push("--model".to_string());
46
+ argv.push(model.to_string());
47
+ }
48
+ if let Some(overrides) = overrides {
49
+ for config in &overrides.codex_config {
50
+ argv.push("-c".to_string());
51
+ argv.push(config.clone());
52
+ }
53
+ }
54
+ if let Some(prompt) = system_prompt {
55
+ // codex.py:120 — escape order matters: backslash first, then quote, then newline.
56
+ let escaped = prompt
57
+ .replace('\\', "\\\\")
58
+ .replace('"', "\\\"")
59
+ .replace('\n', "\\n");
60
+ argv.push("-c".to_string());
61
+ argv.push(format!("developer_instructions=\"{escaped}\""));
62
+ }
63
+ if let Some(config) = mcp_config {
64
+ append_codex_mcp_overrides(&mut argv, &config.raw);
65
+ }
66
+ argv
67
+ }
68
+
69
+ /// Render an `McpConfig::raw` ({ name: { type, command, args, env: {...} } }) into Codex
70
+ /// `-c mcp_servers.<name>.<field>=...` overrides. JSON values are stringified with serde
71
+ /// so arrays/objects survive (Codex parses the right-hand side as JSON; this is what the
72
+ /// Python golden + the live attached Codex panes do).
73
+ pub(crate) fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_json::Value) {
74
+ let Some(servers) = raw.as_object() else {
75
+ return;
76
+ };
77
+ for (name, server) in servers {
78
+ let Some(obj) = server.as_object() else {
79
+ continue;
80
+ };
81
+ for (key, value) in obj {
82
+ if key == "env" {
83
+ if let Some(env) = value.as_object() {
84
+ for (env_key, env_value) in env {
85
+ argv.push("-c".to_string());
86
+ argv.push(format!(
87
+ "mcp_servers.{name}.env.{env_key}={}",
88
+ json_inline(env_value)
89
+ ));
90
+ }
91
+ }
92
+ continue;
93
+ }
94
+ argv.push("-c".to_string());
95
+ argv.push(format!("mcp_servers.{name}.{key}={}", json_inline(value)));
96
+ }
97
+ // Every MCP server gets a 600s tool timeout so long-running
98
+ // team_orchestrator calls (report_result etc.) survive the codex default.
99
+ argv.push("-c".to_string());
100
+ argv.push(format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
101
+ }
102
+ }
103
+
104
+ pub(crate) fn codex_dangerous_auto_approve(tools: &[&str]) -> bool {
105
+ tools.contains(&"dangerous_auto_approve")
106
+ }
107
+
108
+ pub(crate) fn codex_sandbox_mode(tools: &[&str]) -> &'static str {
109
+ if tools
110
+ .iter()
111
+ .any(|tool| matches!(*tool, "fs_write" | "execute_bash"))
112
+ {
113
+ "workspace-write"
114
+ } else {
115
+ "read-only"
116
+ }
117
+ }