agent-dispatch 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- agent_dispatch-0.2.0/.github/dependabot.yml +10 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/.github/workflows/ci.yml +2 -2
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/.github/workflows/publish.yml +3 -3
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/PKG-INFO +54 -5
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/README.md +53 -4
- agent_dispatch-0.2.0/SECURITY.md +22 -0
- agent_dispatch-0.2.0/agents.example.yaml +47 -0
- agent_dispatch-0.2.0/assets/mascot.png +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/pyproject.toml +1 -1
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/__init__.py +1 -1
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/cli.py +132 -1
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/config.py +2 -2
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/models.py +16 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/runner.py +111 -15
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/server.py +167 -33
- agent_dispatch-0.2.0/tests/test_cli.py +203 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/test_models.py +39 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/test_runner.py +118 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/test_server.py +197 -0
- agent_dispatch-0.1.0/agents.example.yaml +0 -24
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/.gitignore +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/LICENSE +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/src/agent_dispatch/cache.py +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/__init__.py +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/conftest.py +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/test_cache.py +0 -0
- {agent_dispatch-0.1.0 → agent_dispatch-0.2.0}/tests/test_config.py +0 -0
|
@@ -14,9 +14,9 @@ jobs:
|
|
|
14
14
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
18
18
|
|
|
19
|
-
- uses: actions/setup-python@
|
|
19
|
+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
20
20
|
with:
|
|
21
21
|
python-version: ${{ matrix.python-version }}
|
|
22
22
|
|
|
@@ -11,9 +11,9 @@ jobs:
|
|
|
11
11
|
permissions:
|
|
12
12
|
id-token: write
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
15
15
|
|
|
16
|
-
- uses: actions/setup-python@
|
|
16
|
+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
17
17
|
with:
|
|
18
18
|
python-version: "3.12"
|
|
19
19
|
|
|
@@ -23,4 +23,4 @@ jobs:
|
|
|
23
23
|
python -m build
|
|
24
24
|
|
|
25
25
|
- name: Publish to PyPI
|
|
26
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-dispatch
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: MCP server that lets Claude Code agents delegate tasks to agents in other project directories
|
|
5
5
|
Project-URL: Homepage, https://github.com/ginkida/agent-dispatch
|
|
6
6
|
Project-URL: Repository, https://github.com/ginkida/agent-dispatch
|
|
@@ -30,8 +30,17 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
# agent-dispatch
|
|
32
32
|
|
|
33
|
+
[](https://pypi.org/project/agent-dispatch/)
|
|
34
|
+
[](https://github.com/ginkida/agent-dispatch/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/agent-dispatch/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
33
38
|
**MCP server that lets Claude Code agents delegate tasks to agents in other project directories.**
|
|
34
39
|
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img src="assets/mascot.png" alt="agent-dispatch mascot" width="600">
|
|
42
|
+
</p>
|
|
43
|
+
|
|
35
44
|
Each agent runs as a separate `claude -p` session in its own project directory — inheriting that project's MCP servers, CLAUDE.md, and tools. The calling agent just gets the result back.
|
|
36
45
|
|
|
37
46
|
Works with OAuth, API key, and Claude subscription authentication.
|
|
@@ -50,6 +59,9 @@ agent-dispatch add backend ~/projects/backend
|
|
|
50
59
|
|
|
51
60
|
# Test it works
|
|
52
61
|
agent-dispatch test infra
|
|
62
|
+
|
|
63
|
+
# If agents hit permission errors, grant tool access:
|
|
64
|
+
agent-dispatch update infra --permission-mode bypassPermissions
|
|
53
65
|
```
|
|
54
66
|
|
|
55
67
|
Done. Every Claude Code session now has access to all dispatch tools.
|
|
@@ -70,7 +82,7 @@ Done. Every Claude Code session now has access to all dispatch tools.
|
|
|
70
82
|
Lists all configured agents. **Call this first** to see what's available.
|
|
71
83
|
|
|
72
84
|
```json
|
|
73
|
-
// Response
|
|
85
|
+
// Response (permission fields shown only when configured)
|
|
74
86
|
[
|
|
75
87
|
{
|
|
76
88
|
"name": "infra",
|
|
@@ -78,7 +90,9 @@ Lists all configured agents. **Call this first** to see what's available.
|
|
|
78
90
|
"description": "Infrastructure agent. MCP: portainer. Stack: Python, Docker",
|
|
79
91
|
"healthy": true,
|
|
80
92
|
"has_claude_md": true,
|
|
81
|
-
"has_mcp_config": true
|
|
93
|
+
"has_mcp_config": true,
|
|
94
|
+
"permission_mode": "bypassPermissions",
|
|
95
|
+
"allowed_tools": ["Bash", "Read", "Grep"]
|
|
82
96
|
}
|
|
83
97
|
]
|
|
84
98
|
```
|
|
@@ -96,7 +110,7 @@ One-shot task delegation. Results are cached — identical requests within TTL r
|
|
|
96
110
|
| `goal` | string | no | Broader objective — helps the agent make better trade-offs |
|
|
97
111
|
|
|
98
112
|
```json
|
|
99
|
-
// Response
|
|
113
|
+
// Response (success)
|
|
100
114
|
{
|
|
101
115
|
"agent": "infra",
|
|
102
116
|
"success": true,
|
|
@@ -106,8 +120,19 @@ One-shot task delegation. Results are cached — identical requests within TTL r
|
|
|
106
120
|
"duration_ms": 5000,
|
|
107
121
|
"num_turns": 2
|
|
108
122
|
}
|
|
123
|
+
|
|
124
|
+
// Response (failure — error_type helps you handle programmatically)
|
|
125
|
+
{
|
|
126
|
+
"agent": "infra",
|
|
127
|
+
"success": false,
|
|
128
|
+
"result": "",
|
|
129
|
+
"error": "Tool_use is not allowed in this permission mode\n\nHint: ...",
|
|
130
|
+
"error_type": "permission"
|
|
131
|
+
}
|
|
109
132
|
```
|
|
110
133
|
|
|
134
|
+
**`error_type` values:** `permission` (tool/action denied), `timeout`, `recursion` (dispatch depth exceeded), `not_found` (missing directory or CLI), `cli_error` (other failures). Permission errors include an actionable hint.
|
|
135
|
+
|
|
111
136
|
**Always pass `caller` and `goal`** — the dispatched agent sees a structured prompt:
|
|
112
137
|
|
|
113
138
|
```markdown
|
|
@@ -231,6 +256,24 @@ Register a new project directory as an agent. Description is auto-generated from
|
|
|
231
256
|
| `name` | string | yes | Agent name (letters, digits, hyphens, underscores) |
|
|
232
257
|
| `directory` | string | yes | Absolute path to project directory |
|
|
233
258
|
| `description` | string | no | What this agent can do — auto-generated if empty |
|
|
259
|
+
| `timeout` | int | no | Timeout in seconds (0 = use global default) |
|
|
260
|
+
| `permission_mode` | string | no | Permission mode (e.g. `default`, `plan`, `bypassPermissions`) |
|
|
261
|
+
| `allowed_tools` | string | no | Comma-separated allowed tools (e.g. `"Bash,Read,Edit"`) |
|
|
262
|
+
| `disallowed_tools` | string | no | Comma-separated disallowed tools |
|
|
263
|
+
|
|
264
|
+
### `update_agent`
|
|
265
|
+
|
|
266
|
+
Update an existing agent's configuration. Only non-empty fields are changed. Pass `"none"` to clear a field.
|
|
267
|
+
|
|
268
|
+
| Parameter | Type | Required | Description |
|
|
269
|
+
|-----------|------|----------|-------------|
|
|
270
|
+
| `name` | string | yes | Agent name to update |
|
|
271
|
+
| `description` | string | no | New description |
|
|
272
|
+
| `timeout` | int | no | New timeout (0 = don't change) |
|
|
273
|
+
| `model` | string | no | Model override. `"none"` to clear |
|
|
274
|
+
| `permission_mode` | string | no | Permission mode. `"none"` to clear |
|
|
275
|
+
| `allowed_tools` | string | no | Comma-separated. `"none"` to clear |
|
|
276
|
+
| `disallowed_tools` | string | no | Comma-separated. `"none"` to clear |
|
|
234
277
|
|
|
235
278
|
### `remove_agent`
|
|
236
279
|
|
|
@@ -284,6 +327,11 @@ agents:
|
|
|
284
327
|
|
|
285
328
|
settings:
|
|
286
329
|
default_timeout: 300
|
|
330
|
+
# default_permission_mode: bypassPermissions # inherited by all agents
|
|
331
|
+
# default_allowed_tools: # inherited when agent has none
|
|
332
|
+
# - Bash
|
|
333
|
+
# - Read
|
|
334
|
+
# - Edit
|
|
287
335
|
max_dispatch_depth: 3 # recursion protection
|
|
288
336
|
max_concurrency: 5 # max parallel claude -p processes
|
|
289
337
|
cache:
|
|
@@ -338,8 +386,9 @@ agent-dispatch MCP server
|
|
|
338
386
|
|---------|-------------|
|
|
339
387
|
| `agent-dispatch init` | Create config + register MCP server with Claude Code |
|
|
340
388
|
| `agent-dispatch add <name> <dir>` | Add an agent (auto-generates description) |
|
|
389
|
+
| `agent-dispatch update <name>` | Update agent config (permissions, timeout, model, etc.) |
|
|
341
390
|
| `agent-dispatch remove <name>` | Remove an agent |
|
|
342
|
-
| `agent-dispatch list` | List agents with health status |
|
|
391
|
+
| `agent-dispatch list` | List agents with health status and permissions |
|
|
343
392
|
| `agent-dispatch test <name> [task]` | Test an agent with a dispatch |
|
|
344
393
|
| `agent-dispatch serve` | Start MCP server (stdio, used by Claude Code) |
|
|
345
394
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# agent-dispatch
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/agent-dispatch/)
|
|
4
|
+
[](https://github.com/ginkida/agent-dispatch/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pypi.org/project/agent-dispatch/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
**MCP server that lets Claude Code agents delegate tasks to agents in other project directories.**
|
|
4
9
|
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img src="assets/mascot.png" alt="agent-dispatch mascot" width="600">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
5
14
|
Each agent runs as a separate `claude -p` session in its own project directory — inheriting that project's MCP servers, CLAUDE.md, and tools. The calling agent just gets the result back.
|
|
6
15
|
|
|
7
16
|
Works with OAuth, API key, and Claude subscription authentication.
|
|
@@ -20,6 +29,9 @@ agent-dispatch add backend ~/projects/backend
|
|
|
20
29
|
|
|
21
30
|
# Test it works
|
|
22
31
|
agent-dispatch test infra
|
|
32
|
+
|
|
33
|
+
# If agents hit permission errors, grant tool access:
|
|
34
|
+
agent-dispatch update infra --permission-mode bypassPermissions
|
|
23
35
|
```
|
|
24
36
|
|
|
25
37
|
Done. Every Claude Code session now has access to all dispatch tools.
|
|
@@ -40,7 +52,7 @@ Done. Every Claude Code session now has access to all dispatch tools.
|
|
|
40
52
|
Lists all configured agents. **Call this first** to see what's available.
|
|
41
53
|
|
|
42
54
|
```json
|
|
43
|
-
// Response
|
|
55
|
+
// Response (permission fields shown only when configured)
|
|
44
56
|
[
|
|
45
57
|
{
|
|
46
58
|
"name": "infra",
|
|
@@ -48,7 +60,9 @@ Lists all configured agents. **Call this first** to see what's available.
|
|
|
48
60
|
"description": "Infrastructure agent. MCP: portainer. Stack: Python, Docker",
|
|
49
61
|
"healthy": true,
|
|
50
62
|
"has_claude_md": true,
|
|
51
|
-
"has_mcp_config": true
|
|
63
|
+
"has_mcp_config": true,
|
|
64
|
+
"permission_mode": "bypassPermissions",
|
|
65
|
+
"allowed_tools": ["Bash", "Read", "Grep"]
|
|
52
66
|
}
|
|
53
67
|
]
|
|
54
68
|
```
|
|
@@ -66,7 +80,7 @@ One-shot task delegation. Results are cached — identical requests within TTL r
|
|
|
66
80
|
| `goal` | string | no | Broader objective — helps the agent make better trade-offs |
|
|
67
81
|
|
|
68
82
|
```json
|
|
69
|
-
// Response
|
|
83
|
+
// Response (success)
|
|
70
84
|
{
|
|
71
85
|
"agent": "infra",
|
|
72
86
|
"success": true,
|
|
@@ -76,8 +90,19 @@ One-shot task delegation. Results are cached — identical requests within TTL r
|
|
|
76
90
|
"duration_ms": 5000,
|
|
77
91
|
"num_turns": 2
|
|
78
92
|
}
|
|
93
|
+
|
|
94
|
+
// Response (failure — error_type helps you handle programmatically)
|
|
95
|
+
{
|
|
96
|
+
"agent": "infra",
|
|
97
|
+
"success": false,
|
|
98
|
+
"result": "",
|
|
99
|
+
"error": "Tool_use is not allowed in this permission mode\n\nHint: ...",
|
|
100
|
+
"error_type": "permission"
|
|
101
|
+
}
|
|
79
102
|
```
|
|
80
103
|
|
|
104
|
+
**`error_type` values:** `permission` (tool/action denied), `timeout`, `recursion` (dispatch depth exceeded), `not_found` (missing directory or CLI), `cli_error` (other failures). Permission errors include an actionable hint.
|
|
105
|
+
|
|
81
106
|
**Always pass `caller` and `goal`** — the dispatched agent sees a structured prompt:
|
|
82
107
|
|
|
83
108
|
```markdown
|
|
@@ -201,6 +226,24 @@ Register a new project directory as an agent. Description is auto-generated from
|
|
|
201
226
|
| `name` | string | yes | Agent name (letters, digits, hyphens, underscores) |
|
|
202
227
|
| `directory` | string | yes | Absolute path to project directory |
|
|
203
228
|
| `description` | string | no | What this agent can do — auto-generated if empty |
|
|
229
|
+
| `timeout` | int | no | Timeout in seconds (0 = use global default) |
|
|
230
|
+
| `permission_mode` | string | no | Permission mode (e.g. `default`, `plan`, `bypassPermissions`) |
|
|
231
|
+
| `allowed_tools` | string | no | Comma-separated allowed tools (e.g. `"Bash,Read,Edit"`) |
|
|
232
|
+
| `disallowed_tools` | string | no | Comma-separated disallowed tools |
|
|
233
|
+
|
|
234
|
+
### `update_agent`
|
|
235
|
+
|
|
236
|
+
Update an existing agent's configuration. Only non-empty fields are changed. Pass `"none"` to clear a field.
|
|
237
|
+
|
|
238
|
+
| Parameter | Type | Required | Description |
|
|
239
|
+
|-----------|------|----------|-------------|
|
|
240
|
+
| `name` | string | yes | Agent name to update |
|
|
241
|
+
| `description` | string | no | New description |
|
|
242
|
+
| `timeout` | int | no | New timeout (0 = don't change) |
|
|
243
|
+
| `model` | string | no | Model override. `"none"` to clear |
|
|
244
|
+
| `permission_mode` | string | no | Permission mode. `"none"` to clear |
|
|
245
|
+
| `allowed_tools` | string | no | Comma-separated. `"none"` to clear |
|
|
246
|
+
| `disallowed_tools` | string | no | Comma-separated. `"none"` to clear |
|
|
204
247
|
|
|
205
248
|
### `remove_agent`
|
|
206
249
|
|
|
@@ -254,6 +297,11 @@ agents:
|
|
|
254
297
|
|
|
255
298
|
settings:
|
|
256
299
|
default_timeout: 300
|
|
300
|
+
# default_permission_mode: bypassPermissions # inherited by all agents
|
|
301
|
+
# default_allowed_tools: # inherited when agent has none
|
|
302
|
+
# - Bash
|
|
303
|
+
# - Read
|
|
304
|
+
# - Edit
|
|
257
305
|
max_dispatch_depth: 3 # recursion protection
|
|
258
306
|
max_concurrency: 5 # max parallel claude -p processes
|
|
259
307
|
cache:
|
|
@@ -308,8 +356,9 @@ agent-dispatch MCP server
|
|
|
308
356
|
|---------|-------------|
|
|
309
357
|
| `agent-dispatch init` | Create config + register MCP server with Claude Code |
|
|
310
358
|
| `agent-dispatch add <name> <dir>` | Add an agent (auto-generates description) |
|
|
359
|
+
| `agent-dispatch update <name>` | Update agent config (permissions, timeout, model, etc.) |
|
|
311
360
|
| `agent-dispatch remove <name>` | Remove an agent |
|
|
312
|
-
| `agent-dispatch list` | List agents with health status |
|
|
361
|
+
| `agent-dispatch list` | List agents with health status and permissions |
|
|
313
362
|
| `agent-dispatch test <name> [task]` | Test an agent with a dispatch |
|
|
314
363
|
| `agent-dispatch serve` | Start MCP server (stdio, used by Claude Code) |
|
|
315
364
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you discover a security vulnerability, please report it via [GitHub Security Advisories](https://github.com/ginkida/agent-dispatch/security/advisories/new).
|
|
6
|
+
|
|
7
|
+
**Do not** open a public issue for security vulnerabilities.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
`agent-dispatch` runs `claude -p` subprocesses in configured directories. Security-relevant areas:
|
|
12
|
+
|
|
13
|
+
- **Command injection** — task/context strings are passed as CLI arguments, not shell-evaluated
|
|
14
|
+
- **Directory traversal** — agent directories are resolved to absolute paths via `Path.resolve()`
|
|
15
|
+
- **Recursion** — `AGENT_DISPATCH_DEPTH` env var prevents infinite dispatch loops
|
|
16
|
+
- **Cost** — `max_budget_usd` limits spending per dispatch
|
|
17
|
+
|
|
18
|
+
## Supported Versions
|
|
19
|
+
|
|
20
|
+
| Version | Supported |
|
|
21
|
+
|---------|-----------|
|
|
22
|
+
| 0.1.x | Yes |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
agents:
|
|
2
|
+
# Infrastructure agent — has Portainer MCP for container management
|
|
3
|
+
infra:
|
|
4
|
+
directory: ~/projects/infra
|
|
5
|
+
description: "Infrastructure agent. MCP servers: portainer. Can check container logs, restart services, inspect Docker state."
|
|
6
|
+
timeout: 300
|
|
7
|
+
|
|
8
|
+
# Backend agent — source code, tests, database
|
|
9
|
+
backend:
|
|
10
|
+
directory: ~/projects/backend
|
|
11
|
+
description: "Backend API agent. Python, FastAPI, PostgreSQL. Can modify code, run tests, check migrations."
|
|
12
|
+
timeout: 300
|
|
13
|
+
model: null # use default model
|
|
14
|
+
|
|
15
|
+
# Agent with permission controls — useful when you want to restrict tool access
|
|
16
|
+
staging-db:
|
|
17
|
+
directory: ~/projects/staging-db
|
|
18
|
+
description: "Read-only database agent. Can query staging DB but not modify data."
|
|
19
|
+
timeout: 120
|
|
20
|
+
permission_mode: bypassPermissions # skip permission prompts (agent runs non-interactively)
|
|
21
|
+
allowed_tools: # only these tools are available
|
|
22
|
+
- Read
|
|
23
|
+
- Grep
|
|
24
|
+
- Bash
|
|
25
|
+
# disallowed_tools: # alternatively, block specific tools
|
|
26
|
+
# - Write
|
|
27
|
+
# - Edit
|
|
28
|
+
|
|
29
|
+
# Frontend agent
|
|
30
|
+
# frontend:
|
|
31
|
+
# directory: ~/projects/frontend
|
|
32
|
+
# description: "Frontend React app. Can modify components, run build, check TypeScript errors."
|
|
33
|
+
# timeout: 180
|
|
34
|
+
|
|
35
|
+
settings:
|
|
36
|
+
default_timeout: 300
|
|
37
|
+
max_dispatch_depth: 3 # recursion protection: A -> B -> A
|
|
38
|
+
max_concurrency: 5 # max parallel claude -p processes
|
|
39
|
+
# default_max_budget_usd: 1.0 # cost limit per dispatch (all agents)
|
|
40
|
+
# default_permission_mode: bypassPermissions # inherited by agents without override
|
|
41
|
+
# default_allowed_tools: # inherited by agents without override
|
|
42
|
+
# - Bash
|
|
43
|
+
# - Read
|
|
44
|
+
# - Edit
|
|
45
|
+
cache:
|
|
46
|
+
enabled: true
|
|
47
|
+
ttl: 300 # seconds; identical (agent, task, context) requests are cached
|
|
Binary file
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
import click
|
|
11
11
|
|
|
12
12
|
from .config import auto_describe, config_path, load_config, save_config
|
|
13
|
-
from .models import AgentConfig, DispatchConfig, validate_agent_name
|
|
13
|
+
from .models import AgentConfig, DispatchConfig, check_permission_mode, validate_agent_name
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@click.group()
|
|
@@ -76,12 +76,29 @@ def init() -> None:
|
|
|
76
76
|
@click.option("-d", "--description", default=None, help="Agent description. Auto-generated if omitted.")
|
|
77
77
|
@click.option("--timeout", default=300, help="Timeout in seconds (default: 300).")
|
|
78
78
|
@click.option("--model", default=None, help="Model override for this agent.")
|
|
79
|
+
@click.option("--max-budget", default=None, type=float, help="Max cost in USD per dispatch.")
|
|
80
|
+
@click.option(
|
|
81
|
+
"--permission-mode", default=None,
|
|
82
|
+
help="Permission mode for claude CLI (e.g. default, plan, bypassPermissions).",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--allowed-tools", default=None,
|
|
86
|
+
help="Comma-separated list of allowed tools (e.g. Bash,Read,Edit).",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--disallowed-tools", default=None,
|
|
90
|
+
help="Comma-separated list of disallowed tools.",
|
|
91
|
+
)
|
|
79
92
|
def add(
|
|
80
93
|
name: str,
|
|
81
94
|
directory: str,
|
|
82
95
|
description: str | None,
|
|
83
96
|
timeout: int,
|
|
84
97
|
model: str | None,
|
|
98
|
+
max_budget: float | None,
|
|
99
|
+
permission_mode: str | None,
|
|
100
|
+
allowed_tools: str | None,
|
|
101
|
+
disallowed_tools: str | None,
|
|
85
102
|
) -> None:
|
|
86
103
|
"""Add an agent. Auto-generates description from project files if omitted."""
|
|
87
104
|
try:
|
|
@@ -106,7 +123,16 @@ def add(
|
|
|
106
123
|
description=description,
|
|
107
124
|
timeout=timeout,
|
|
108
125
|
model=model,
|
|
126
|
+
max_budget_usd=max_budget,
|
|
127
|
+
permission_mode=permission_mode,
|
|
128
|
+
allowed_tools=[t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
129
|
+
if allowed_tools else [],
|
|
130
|
+
disallowed_tools=[t.strip() for t in disallowed_tools.split(",") if t.strip()]
|
|
131
|
+
if disallowed_tools else [],
|
|
109
132
|
)
|
|
133
|
+
if warning := check_permission_mode(permission_mode):
|
|
134
|
+
click.echo(click.style(f"Warning: {warning}", fg="yellow"))
|
|
135
|
+
|
|
110
136
|
save_config(config)
|
|
111
137
|
click.echo(f"Added agent '{name}' -> {dir_path}")
|
|
112
138
|
|
|
@@ -139,9 +165,104 @@ def list_agents() -> None:
|
|
|
139
165
|
click.echo(f" {name} [{status}]")
|
|
140
166
|
click.echo(f" dir: {agent.directory}")
|
|
141
167
|
click.echo(f" desc: {agent.description}")
|
|
168
|
+
extras: list[str] = []
|
|
169
|
+
if agent.timeout != 300:
|
|
170
|
+
extras.append(f"timeout={agent.timeout}s")
|
|
171
|
+
if agent.model:
|
|
172
|
+
extras.append(f"model={agent.model}")
|
|
173
|
+
if agent.max_budget_usd:
|
|
174
|
+
extras.append(f"budget=${agent.max_budget_usd}")
|
|
175
|
+
if extras:
|
|
176
|
+
click.echo(f" config: {', '.join(extras)}")
|
|
177
|
+
if agent.permission_mode:
|
|
178
|
+
click.echo(f" permission_mode: {agent.permission_mode}")
|
|
179
|
+
if agent.allowed_tools:
|
|
180
|
+
click.echo(f" allowed_tools: {', '.join(agent.allowed_tools)}")
|
|
181
|
+
if agent.disallowed_tools:
|
|
182
|
+
click.echo(f" disallowed_tools: {', '.join(agent.disallowed_tools)}")
|
|
142
183
|
click.echo()
|
|
143
184
|
|
|
144
185
|
|
|
186
|
+
@cli.command()
|
|
187
|
+
@click.argument("name")
|
|
188
|
+
@click.option("-d", "--description", default=None, help="New description.")
|
|
189
|
+
@click.option("--timeout", default=None, type=int, help="Timeout in seconds.")
|
|
190
|
+
@click.option("--model", default=None, help="Model override.")
|
|
191
|
+
@click.option("--max-budget", default=None, type=float, help="Max cost in USD. Use 0 to clear.")
|
|
192
|
+
@click.option(
|
|
193
|
+
"--permission-mode", default=None,
|
|
194
|
+
help="Permission mode (default, plan, bypassPermissions). Use 'none' to clear.",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"--allowed-tools", default=None,
|
|
198
|
+
help="Comma-separated allowed tools. Use 'none' to clear.",
|
|
199
|
+
)
|
|
200
|
+
@click.option(
|
|
201
|
+
"--disallowed-tools", default=None,
|
|
202
|
+
help="Comma-separated disallowed tools. Use 'none' to clear.",
|
|
203
|
+
)
|
|
204
|
+
@click.pass_context
|
|
205
|
+
def update(
|
|
206
|
+
ctx: click.Context,
|
|
207
|
+
name: str,
|
|
208
|
+
description: str | None,
|
|
209
|
+
timeout: int | None,
|
|
210
|
+
model: str | None,
|
|
211
|
+
max_budget: float | None,
|
|
212
|
+
permission_mode: str | None,
|
|
213
|
+
allowed_tools: str | None,
|
|
214
|
+
disallowed_tools: str | None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Update an existing agent's configuration."""
|
|
217
|
+
config = load_config()
|
|
218
|
+
if name not in config.agents:
|
|
219
|
+
click.echo(f"Agent '{name}' not found. Run 'agent-dispatch list' to see agents.")
|
|
220
|
+
raise SystemExit(1)
|
|
221
|
+
|
|
222
|
+
agent = config.agents[name]
|
|
223
|
+
updated: list[str] = []
|
|
224
|
+
|
|
225
|
+
if description is not None:
|
|
226
|
+
agent.description = description
|
|
227
|
+
updated.append("description")
|
|
228
|
+
if timeout is not None:
|
|
229
|
+
agent.timeout = timeout
|
|
230
|
+
updated.append("timeout")
|
|
231
|
+
if model is not None:
|
|
232
|
+
agent.model = None if model.lower() == "none" else model
|
|
233
|
+
updated.append("model")
|
|
234
|
+
if max_budget is not None:
|
|
235
|
+
agent.max_budget_usd = None if max_budget == 0 else max_budget
|
|
236
|
+
updated.append("max_budget_usd")
|
|
237
|
+
if permission_mode is not None:
|
|
238
|
+
effective = None if permission_mode.lower() == "none" else permission_mode
|
|
239
|
+
agent.permission_mode = effective
|
|
240
|
+
if warning := check_permission_mode(effective):
|
|
241
|
+
click.echo(click.style(f"Warning: {warning}", fg="yellow"))
|
|
242
|
+
updated.append("permission_mode")
|
|
243
|
+
if allowed_tools is not None:
|
|
244
|
+
if allowed_tools.lower() == "none":
|
|
245
|
+
agent.allowed_tools = []
|
|
246
|
+
else:
|
|
247
|
+
agent.allowed_tools = [t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
248
|
+
updated.append("allowed_tools")
|
|
249
|
+
if disallowed_tools is not None:
|
|
250
|
+
if disallowed_tools.lower() == "none":
|
|
251
|
+
agent.disallowed_tools = []
|
|
252
|
+
else:
|
|
253
|
+
agent.disallowed_tools = [
|
|
254
|
+
t.strip() for t in disallowed_tools.split(",") if t.strip()
|
|
255
|
+
]
|
|
256
|
+
updated.append("disallowed_tools")
|
|
257
|
+
|
|
258
|
+
if not updated:
|
|
259
|
+
click.echo("Nothing to update. Pass at least one option (see --help).")
|
|
260
|
+
raise SystemExit(1)
|
|
261
|
+
|
|
262
|
+
save_config(config)
|
|
263
|
+
click.echo(f"Updated agent '{name}': {', '.join(updated)}")
|
|
264
|
+
|
|
265
|
+
|
|
145
266
|
@cli.command()
|
|
146
267
|
@click.argument("name")
|
|
147
268
|
@click.argument("task", default="What project is this? Describe in one sentence.")
|
|
@@ -167,6 +288,16 @@ def test(name: str, task: str) -> None:
|
|
|
167
288
|
click.echo(f"\n--- Cost: ${result.cost_usd:.4f} | Turns: {result.num_turns}")
|
|
168
289
|
else:
|
|
169
290
|
click.echo(click.style(f"Error: {result.error}", fg="red"))
|
|
291
|
+
if result.error_type == "permission":
|
|
292
|
+
click.echo()
|
|
293
|
+
click.echo(click.style("Diagnosis: permission error", fg="yellow"))
|
|
294
|
+
click.echo("The agent was denied a tool or action. To fix:")
|
|
295
|
+
click.echo(f" agent-dispatch update {name} --permission-mode bypassPermissions")
|
|
296
|
+
click.echo(f" agent-dispatch update {name} --allowed-tools Bash,Read,Edit,Write")
|
|
297
|
+
elif result.error_type == "timeout":
|
|
298
|
+
click.echo()
|
|
299
|
+
click.echo(click.style("Diagnosis: timeout", fg="yellow"))
|
|
300
|
+
click.echo(f" agent-dispatch update {name} --timeout 600")
|
|
170
301
|
raise SystemExit(1)
|
|
171
302
|
|
|
172
303
|
|
|
@@ -56,7 +56,7 @@ def _collect_mcp_servers(directory: Path) -> list[str]:
|
|
|
56
56
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
57
57
|
servers.extend(data.get("mcpServers", {}).keys())
|
|
58
58
|
except (json.JSONDecodeError, KeyError):
|
|
59
|
-
|
|
59
|
+
logger.debug("Failed to parse MCP config: %s", path)
|
|
60
60
|
return list(dict.fromkeys(servers)) # deduplicate, preserve order
|
|
61
61
|
|
|
62
62
|
|
|
@@ -115,7 +115,7 @@ def auto_describe(directory: Path) -> str:
|
|
|
115
115
|
if pkg.get("description"):
|
|
116
116
|
parts.append(pkg["description"])
|
|
117
117
|
except (json.JSONDecodeError, KeyError):
|
|
118
|
-
|
|
118
|
+
logger.debug("Failed to parse package.json: %s", pkg_json)
|
|
119
119
|
|
|
120
120
|
# MCP servers — critical for understanding what tools this agent has
|
|
121
121
|
servers = _collect_mcp_servers(directory)
|
|
@@ -8,6 +8,18 @@ from pydantic import BaseModel, Field, field_validator
|
|
|
8
8
|
|
|
9
9
|
_AGENT_NAME_PATTERN = r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
|
|
10
10
|
|
|
11
|
+
KNOWN_PERMISSION_MODES = frozenset({
|
|
12
|
+
"default", "plan", "bypassPermissions",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_permission_mode(mode: str | None) -> str | None:
|
|
17
|
+
"""Return a warning message if mode is unknown, else None."""
|
|
18
|
+
if mode and mode not in KNOWN_PERMISSION_MODES:
|
|
19
|
+
known = ", ".join(sorted(KNOWN_PERMISSION_MODES))
|
|
20
|
+
return f"Unknown permission_mode: {mode!r}. Known values: {known}"
|
|
21
|
+
return None
|
|
22
|
+
|
|
11
23
|
|
|
12
24
|
class AgentConfig(BaseModel):
|
|
13
25
|
"""Configuration for a single agent."""
|
|
@@ -46,6 +58,9 @@ class Settings(BaseModel):
|
|
|
46
58
|
|
|
47
59
|
default_timeout: int = 300
|
|
48
60
|
default_max_budget_usd: float | None = None
|
|
61
|
+
default_permission_mode: str | None = None
|
|
62
|
+
default_allowed_tools: list[str] = Field(default_factory=list)
|
|
63
|
+
default_disallowed_tools: list[str] = Field(default_factory=list)
|
|
49
64
|
max_dispatch_depth: int = Field(default=3, ge=1)
|
|
50
65
|
max_concurrency: int = Field(default=5, ge=1)
|
|
51
66
|
cache: CacheSettings = Field(default_factory=CacheSettings)
|
|
@@ -82,3 +97,4 @@ class DispatchResult(BaseModel):
|
|
|
82
97
|
duration_ms: int | None = None
|
|
83
98
|
num_turns: int | None = None
|
|
84
99
|
error: str | None = None
|
|
100
|
+
error_type: str | None = None # permission, timeout, recursion, not_found, cli_error
|