zwarm 1.0.1__py3-none-any.whl → 1.2.1__py3-none-any.whl

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.
zwarm/core/config.py CHANGED
@@ -62,6 +62,12 @@ class OrchestratorConfig:
62
62
  sync_first: bool = True # prefer sync mode by default
63
63
  compaction: CompactionConfig = field(default_factory=CompactionConfig)
64
64
 
65
+ # Directory restrictions for agent delegations
66
+ # None = only working_dir allowed (most restrictive, default)
67
+ # ["*"] = any directory allowed (dangerous)
68
+ # ["/path/a", "/path/b"] = only these directories allowed
69
+ allowed_dirs: list[str] | None = None
70
+
65
71
 
66
72
  @dataclass
67
73
  class WatcherConfigItem:
@@ -188,9 +194,24 @@ def load_env(path: Path | None = None) -> None:
188
194
 
189
195
 
190
196
  def load_toml_config(path: Path | None = None) -> dict[str, Any]:
191
- """Load config.toml file."""
197
+ """
198
+ Load config.toml file.
199
+
200
+ Search order:
201
+ 1. Explicit path (if provided)
202
+ 2. .zwarm/config.toml (new standard location)
203
+ 3. config.toml (legacy location for backwards compat)
204
+ """
192
205
  if path is None:
193
- path = Path.cwd() / "config.toml"
206
+ # Try new location first
207
+ new_path = Path.cwd() / ".zwarm" / "config.toml"
208
+ legacy_path = Path.cwd() / "config.toml"
209
+ if new_path.exists():
210
+ path = new_path
211
+ elif legacy_path.exists():
212
+ path = legacy_path
213
+ else:
214
+ return {}
194
215
  if not path.exists():
195
216
  return {}
196
217
  with open(path, "rb") as f:
zwarm/orchestrator.py CHANGED
@@ -21,9 +21,7 @@ from pydantic import Field, PrivateAttr
21
21
  from wbal.agents.yaml_agent import YamlAgent
22
22
  from wbal.helper import TOOL_CALL_TYPE, format_openai_tool_response
23
23
 
24
- from zwarm.adapters.base import ExecutorAdapter
25
- from zwarm.adapters.claude_code import ClaudeCodeAdapter
26
- from zwarm.adapters.codex_mcp import CodexMCPAdapter
24
+ from zwarm.adapters import ExecutorAdapter, get_adapter
27
25
  from zwarm.core.compact import compact_messages, should_compact
28
26
  from zwarm.core.config import ZwarmConfig, load_config
29
27
  from zwarm.core.environment import OrchestratorEnv
@@ -123,16 +121,11 @@ class Orchestrator(YamlAgent):
123
121
  return self._state
124
122
 
125
123
  def _get_adapter(self, name: str) -> ExecutorAdapter:
126
- """Get or create an adapter by name."""
124
+ """Get or create an adapter by name using the adapter registry."""
127
125
  if name not in self._adapters:
128
126
  # Get model from config (adapters have their own defaults if None)
129
127
  model = self.config.executor.model
130
- if name == "codex_mcp":
131
- self._adapters[name] = CodexMCPAdapter(model=model)
132
- elif name == "claude_code":
133
- self._adapters[name] = ClaudeCodeAdapter(model=model)
134
- else:
135
- raise ValueError(f"Unknown adapter: {name}")
128
+ self._adapters[name] = get_adapter(name, model=model)
136
129
  return self._adapters[name]
137
130
 
138
131
  def get_executor_usage(self) -> dict[str, int]:
zwarm/tools/delegation.py CHANGED
@@ -11,6 +11,7 @@ These are the core tools that orchestrators use to delegate work to executors:
11
11
  from __future__ import annotations
12
12
 
13
13
  import asyncio
14
+ from pathlib import Path
14
15
  from typing import TYPE_CHECKING, Any, Literal
15
16
 
16
17
  from wbal.helper import weaveTool
@@ -31,6 +32,65 @@ def _format_session_header(session_id: str, adapter: str, mode: str) -> str:
31
32
  return f"[{session_id[:8]}] {adapter} ({mode})"
32
33
 
33
34
 
35
+ def _validate_working_dir(
36
+ requested_dir: Path | str | None,
37
+ default_dir: Path,
38
+ allowed_dirs: list[str] | None,
39
+ ) -> tuple[Path, str | None]:
40
+ """
41
+ Validate requested working directory against allowed_dirs config.
42
+
43
+ Args:
44
+ requested_dir: Directory requested by the agent (or None for default)
45
+ default_dir: The orchestrator's working directory
46
+ allowed_dirs: Config setting - None means only default allowed,
47
+ ["*"] means any, or list of allowed paths
48
+
49
+ Returns:
50
+ (validated_path, error_message) - error is None if valid
51
+ """
52
+ if requested_dir is None:
53
+ return default_dir, None
54
+
55
+ requested = Path(requested_dir).resolve()
56
+
57
+ # Check if directory exists
58
+ if not requested.exists():
59
+ return default_dir, f"Directory does not exist: {requested}"
60
+
61
+ if not requested.is_dir():
62
+ return default_dir, f"Not a directory: {requested}"
63
+
64
+ # If allowed_dirs is None, only default is allowed
65
+ if allowed_dirs is None:
66
+ if requested == default_dir.resolve():
67
+ return requested, None
68
+ return default_dir, (
69
+ f"Directory not allowed: {requested}. "
70
+ f"Agent can only delegate to working directory ({default_dir}). "
71
+ "Set orchestrator.allowed_dirs in config to allow other directories."
72
+ )
73
+
74
+ # If ["*"], any directory is allowed
75
+ if allowed_dirs == ["*"]:
76
+ return requested, None
77
+
78
+ # Check against allowed list
79
+ for allowed in allowed_dirs:
80
+ allowed_path = Path(allowed).resolve()
81
+ # Allow if requested is the allowed path or a subdirectory of it
82
+ try:
83
+ requested.relative_to(allowed_path)
84
+ return requested, None
85
+ except ValueError:
86
+ continue
87
+
88
+ return default_dir, (
89
+ f"Directory not allowed: {requested}. "
90
+ f"Allowed directories: {allowed_dirs}"
91
+ )
92
+
93
+
34
94
  @weaveTool
35
95
  def delegate(
36
96
  self: "Orchestrator",
@@ -38,6 +98,7 @@ def delegate(
38
98
  mode: Literal["sync", "async"] = "sync",
39
99
  adapter: str | None = None,
40
100
  model: str | None = None,
101
+ working_dir: str | None = None,
41
102
  ) -> dict[str, Any]:
42
103
  """
43
104
  Delegate work to an executor agent.
@@ -57,6 +118,8 @@ def delegate(
57
118
  mode: "sync" for conversational, "async" for fire-and-forget.
58
119
  adapter: Which executor adapter to use (default: config setting).
59
120
  model: Model override for the executor.
121
+ working_dir: Directory for the executor to work in (default: orchestrator's dir).
122
+ NOTE: May be restricted by orchestrator.allowed_dirs config.
60
123
 
61
124
  Returns:
62
125
  {session_id, status, response (if sync)}
@@ -65,6 +128,20 @@ def delegate(
65
128
  delegate(task="Add a logout button to the navbar", mode="sync")
66
129
  # Then use converse() to refine: "Also add a confirmation dialog"
67
130
  """
131
+ # Validate working directory against allowed_dirs config
132
+ effective_dir, dir_error = _validate_working_dir(
133
+ working_dir,
134
+ self.working_dir,
135
+ self.config.orchestrator.allowed_dirs,
136
+ )
137
+
138
+ if dir_error:
139
+ return {
140
+ "success": False,
141
+ "error": dir_error,
142
+ "hint": "Use the default working directory or ask user to update allowed_dirs config",
143
+ }
144
+
68
145
  # Get adapter (use default from config if not specified)
69
146
  adapter_name = adapter or self.config.executor.adapter
70
147
  executor = self._get_adapter(adapter_name)
@@ -73,7 +150,7 @@ def delegate(
73
150
  session = asyncio.run(
74
151
  executor.start_session(
75
152
  task=task,
76
- working_dir=self.working_dir,
153
+ working_dir=effective_dir,
77
154
  mode=mode,
78
155
  model=model or self.config.executor.model,
79
156
  sandbox=self.config.executor.sandbox,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zwarm
3
- Version: 1.0.1
3
+ Version: 1.2.1
4
4
  Summary: Multi-Agent CLI Orchestration Research Platform
5
5
  Requires-Python: <3.14,>=3.13
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -76,10 +76,11 @@ echo "Fix the bug in auth.py" | zwarm orchestrate
76
76
 
77
77
  zwarm looks for configuration in this order:
78
78
  1. `--config` flag (YAML file)
79
- 2. `config.toml` in working directory
80
- 3. Default settings
79
+ 2. `.zwarm/config.toml` (created by `zwarm init`)
80
+ 3. `config.toml` in working directory (legacy, for backwards compat)
81
+ 4. Default settings
81
82
 
82
- ### Minimal config.toml
83
+ ### Minimal .zwarm/config.toml
83
84
 
84
85
  ```toml
85
86
  [weave]
@@ -430,10 +431,11 @@ zwarm clean --events
430
431
 
431
432
  ### State Management
432
433
 
433
- All state is stored in flat files under `.zwarm/`:
434
+ All state and config is stored in flat files under `.zwarm/`:
434
435
 
435
436
  ```
436
437
  .zwarm/
438
+ ├── config.toml # Runtime settings (weave, adapter, watchers)
437
439
  ├── state.json # Current state
438
440
  ├── events.jsonl # Append-only event log
439
441
  ├── sessions/
@@ -1,16 +1,18 @@
1
1
  zwarm/__init__.py,sha256=3i3LMjHwIzE-LFIS2aUrwv3EZmpkvVMe-xj1h97rcSM,837
2
- zwarm/orchestrator.py,sha256=O3HGjMenQE-HF5xo0WQNMAHRS_vLsUrFfVp6oI_WpKU,19732
2
+ zwarm/orchestrator.py,sha256=3Ix07GCnt7GHoBg2TwFmDaH9TAk6dT5BhuCaLiQQFCw,19430
3
3
  zwarm/test_orchestrator_watchers.py,sha256=QpoaehPU7ekT4XshbTOWnJ2H0wRveV3QOZjxbgyJJLY,807
4
- zwarm/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ zwarm/adapters/__init__.py,sha256=O0b-SfZpb6txeNqFkXZ2aaf34yLFYreznyrAV25jF_Q,656
5
5
  zwarm/adapters/base.py,sha256=fZlQviTgVvOcwnxduTla6WuM6FzQJ_yoHMW5SxwVgQg,2527
6
- zwarm/adapters/claude_code.py,sha256=Z6f_jydiIUuZiR8fNipvvlcv16rP4BK6jNtkUaH3M4c,11368
7
- zwarm/adapters/codex_mcp.py,sha256=Wp4PExZlhqfLV5CHxBqmIBOm-pH7k8cJy93fyxRbCgw,27495
8
- zwarm/adapters/test_codex_mcp.py,sha256=vodQF5VUrM_F1GygUADtXYrA0kvAc7dWpT_Ff-Uoo0A,8383
6
+ zwarm/adapters/claude_code.py,sha256=vAjsjD-_JjARmC4_FBSILQZmQCBrk_oNHo18a9ubuqk,11481
7
+ zwarm/adapters/codex_mcp.py,sha256=f6ScDWWryA6AVWxBNMuvw3FO0a3WAJbeTq4Ij0AR50k,30694
8
+ zwarm/adapters/registry.py,sha256=EdyHECaNA5Kv1od64pYFBJyA_r_6I1r_eJTNP1XYLr4,1781
9
+ zwarm/adapters/test_codex_mcp.py,sha256=0qhVzxn_KF-XUS30gXSJKwMdR3kWGsDY9iPk1Ihqn3w,10698
10
+ zwarm/adapters/test_registry.py,sha256=otxcVDONwFCMisyANToF3iy7Y8dSbCL8bTmZNhxNuF4,2383
9
11
  zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- zwarm/cli/main.py,sha256=dU_8XLIYzcxikl_MgT6BmChNbqYq94dJ4Ra5STzm_KI,33109
12
+ zwarm/cli/main.py,sha256=TE4qIWu11sEKqt2Npy3bNmB0F7IgsSylpupkrP_OoRI,55291
11
13
  zwarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
14
  zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
13
- zwarm/core/config.py,sha256=H8XFsWeEehZnUsMf7nsM_YqjljFYaC43DLs8Xw4sOuQ,10360
15
+ zwarm/core/config.py,sha256=J-TAwIQFLY4x_G9qdvP_16DAYndqg_zhakCW5TLEMN4,11059
14
16
  zwarm/core/environment.py,sha256=HVDpDZEpDSfyh9-wHZMzMKVUPKvioBkPVWeiME2JmFo,5435
15
17
  zwarm/core/models.py,sha256=PrC3okRBVJxISUa1Fax4KkagqLT6Xub-kTxC9drN0sY,10083
16
18
  zwarm/core/state.py,sha256=wMryIvXP-VDLh2b76A5taeL_9dm5k4jk4HnvHWgLqGE,7658
@@ -20,14 +22,14 @@ zwarm/core/test_models.py,sha256=sWTIhMZvuLP5AooGR6y8OR2EyWydqVfhmGrE7NPBBnk,845
20
22
  zwarm/prompts/__init__.py,sha256=FiaIOniLrIyfD3_osxT6I7FfyKjtctbf8jNs5QTPs_s,213
21
23
  zwarm/prompts/orchestrator.py,sha256=R-ym3nlspYNspf085Qwwfi96Yh3K6-Csb6vfBg29K2c,14228
22
24
  zwarm/tools/__init__.py,sha256=FpqxwXJA6-fQ7C-oLj30jjK_0qqcE7MbI0dQuaB56kU,290
23
- zwarm/tools/delegation.py,sha256=wf43jxmGvEy_c8jVx301YQvslIS2F80Qnq4Py2feVvg,11435
25
+ zwarm/tools/delegation.py,sha256=eoImpOcTpPcI8wZTP57PinQ5FzvnYM-qWBEc_xyuUU4,14006
24
26
  zwarm/watchers/__init__.py,sha256=yYGTbhuImQLESUdtfrYbHYBJNvCNX3B-Ei-vY5BizX8,760
25
27
  zwarm/watchers/base.py,sha256=r1GoPlj06nOT2xp4fghfSjxbRyFFFQUB6HpZbEyO2OY,3834
26
28
  zwarm/watchers/builtin.py,sha256=52hyRREYYDsSuG-YKElXViSTyMmGySZaFreHc0pz-A4,12482
27
29
  zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
28
30
  zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
29
31
  zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
30
- zwarm-1.0.1.dist-info/METADATA,sha256=Th-qgFSOQNlTs2nJ_eWEtIPoE0kC_aaqvqDOShzgbGU,14995
31
- zwarm-1.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
- zwarm-1.0.1.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
33
- zwarm-1.0.1.dist-info/RECORD,,
32
+ zwarm-1.2.1.dist-info/METADATA,sha256=G8rZQVHQOPXt7_bOUdL9wEtp6xyXqpzYoCQLx82ay24,15174
33
+ zwarm-1.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ zwarm-1.2.1.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
35
+ zwarm-1.2.1.dist-info/RECORD,,
File without changes