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/adapters/__init__.py +21 -0
- zwarm/adapters/claude_code.py +5 -3
- zwarm/adapters/codex_mcp.py +98 -10
- zwarm/adapters/registry.py +69 -0
- zwarm/adapters/test_codex_mcp.py +50 -0
- zwarm/adapters/test_registry.py +68 -0
- zwarm/cli/main.py +553 -25
- zwarm/core/config.py +23 -2
- zwarm/orchestrator.py +3 -10
- zwarm/tools/delegation.py +78 -1
- {zwarm-1.0.1.dist-info → zwarm-1.2.1.dist-info}/METADATA +7 -5
- {zwarm-1.0.1.dist-info → zwarm-1.2.1.dist-info}/RECORD +14 -12
- {zwarm-1.0.1.dist-info → zwarm-1.2.1.dist-info}/WHEEL +0 -0
- {zwarm-1.0.1.dist-info → zwarm-1.2.1.dist-info}/entry_points.txt +0 -0
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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=
|
|
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.
|
|
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.
|
|
80
|
-
3.
|
|
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=
|
|
2
|
+
zwarm/orchestrator.py,sha256=3Ix07GCnt7GHoBg2TwFmDaH9TAk6dT5BhuCaLiQQFCw,19430
|
|
3
3
|
zwarm/test_orchestrator_watchers.py,sha256=QpoaehPU7ekT4XshbTOWnJ2H0wRveV3QOZjxbgyJJLY,807
|
|
4
|
-
zwarm/adapters/__init__.py,sha256=
|
|
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=
|
|
7
|
-
zwarm/adapters/codex_mcp.py,sha256=
|
|
8
|
-
zwarm/adapters/
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
31
|
-
zwarm-1.
|
|
32
|
-
zwarm-1.
|
|
33
|
-
zwarm-1.
|
|
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
|
|
File without changes
|