dispatchbox 0.1.0__tar.gz → 0.1.2__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.
Files changed (35) hide show
  1. {dispatchbox-0.1.0/dispatchbox.egg-info → dispatchbox-0.1.2}/PKG-INFO +2 -2
  2. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/README.md +1 -1
  3. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/__init__.py +1 -1
  4. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/server.py +8 -1
  5. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/worker.py +19 -0
  6. {dispatchbox-0.1.0 → dispatchbox-0.1.2/dispatchbox.egg-info}/PKG-INFO +2 -2
  7. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox.egg-info/SOURCES.txt +1 -0
  8. dispatchbox-0.1.2/examples/hello-codex.json +26 -0
  9. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/pyproject.toml +1 -1
  10. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/test_e2e.py +13 -0
  11. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/test_step0.py +16 -2
  12. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/LICENSE +0 -0
  13. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/MANIFEST.in +0 -0
  14. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/__main__.py +0 -0
  15. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/chat.py +0 -0
  16. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/cron.py +0 -0
  17. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/feishu.py +0 -0
  18. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/hub.py +0 -0
  19. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/net.py +0 -0
  20. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/rules.py +0 -0
  21. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/slack.py +0 -0
  22. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox/store.py +0 -0
  23. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox.egg-info/dependency_links.txt +0 -0
  24. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox.egg-info/entry_points.txt +0 -0
  25. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox.egg-info/requires.txt +0 -0
  26. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/dispatchbox.egg-info/top_level.txt +0 -0
  27. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/examples/hello.json +0 -0
  28. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/rules.example.json +0 -0
  29. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/setup.cfg +0 -0
  30. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/__init__.py +0 -0
  31. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/helpers.py +0 -0
  32. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/test_chat.py +0 -0
  33. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/test_cron_rules.py +0 -0
  34. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/test_hub.py +0 -0
  35. {dispatchbox-0.1.0 → dispatchbox-0.1.2}/tests/test_net.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dispatchbox
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Route events to Claude Code and Codex running on your own machines, and ask you only when it matters.
5
5
  Author: Xixian Yang
6
6
  License-Expression: MIT
@@ -93,7 +93,7 @@ Most developers already have `claude` or `codex` signed in on their workstation
93
93
 
94
94
  **Prerequisites:**
95
95
  - Python **3.10+** (uses the Python standard library only; no `pip install` required).
96
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex CLI](https://github.com/openai/codex) (`codex`) installed and signed in (`examples/hello.json` uses `claude`).
96
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex CLI](https://github.com/openai/codex) (`codex`) installed and signed in. `examples/hello.json` uses `claude`; for Codex, use [`examples/hello-codex.json`](https://github.com/XixianWasTaken/dispatchbox/blob/main/examples/hello-codex.json) in the commands below.
97
97
 
98
98
  ### 1. Create a scratch workspace and shared token
99
99
 
@@ -63,7 +63,7 @@ Most developers already have `claude` or `codex` signed in on their workstation
63
63
 
64
64
  **Prerequisites:**
65
65
  - Python **3.10+** (uses the Python standard library only; no `pip install` required).
66
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex CLI](https://github.com/openai/codex) (`codex`) installed and signed in (`examples/hello.json` uses `claude`).
66
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex CLI](https://github.com/openai/codex) (`codex`) installed and signed in. `examples/hello.json` uses `claude`; for Codex, use [`examples/hello-codex.json`](https://github.com/XixianWasTaken/dispatchbox/blob/main/examples/hello-codex.json) in the commands below.
67
67
 
68
68
  ### 1. Create a scratch workspace and shared token
69
69
 
@@ -1,3 +1,3 @@
1
1
  """dispatchbox: a thin event router that hands work to coding agents on your own machines."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -151,7 +151,7 @@ class Handler(BaseHTTPRequestHandler):
151
151
  try:
152
152
  self._route(method, parts, query)
153
153
  except HubError as e:
154
- if parts[:1] in (["api"], ["events"], ["claim"], ["hooks"]) or self.headers.get("Accept", "").startswith("application/json"):
154
+ if self._wants_json(parts):
155
155
  self._json(e.status, {"error": str(e)})
156
156
  else:
157
157
  self._html(e.status, page("Error", f"<p>{html.escape(str(e))}</p><p><a href='/'>Back</a></p>"))
@@ -161,6 +161,13 @@ class Handler(BaseHTTPRequestHandler):
161
161
  log.exception("request failed")
162
162
  self._json(500, {"error": f"internal error: {e.__class__.__name__}"})
163
163
 
164
+ def _wants_json(self, parts: list[str]) -> bool:
165
+ """API clients get JSON errors; only a browser using the dashboard gets an HTML page."""
166
+ return (parts[:1] in (["api"], ["events"], ["claim"], ["hooks"])
167
+ or self.headers.get("Accept", "").startswith("application/json")
168
+ or self.headers.get("Content-Type", "").startswith("application/json")
169
+ or self.headers.get("Authorization", "").startswith("Bearer "))
170
+
164
171
  def _route(self, method: str, parts: list[str], q: dict[str, str]) -> None:
165
172
  if parts == ["healthz"]:
166
173
  return self._json(200, {"ok": True})
@@ -132,6 +132,7 @@ def build_command(task: dict[str, Any], cfg: WorkerConfig, tmpdir: str) -> tuple
132
132
  cmd = [cfg.codex_bin, "exec"]
133
133
  if resume:
134
134
  cmd += ["resume", resume]
135
+ extra = _sandbox_as_config(extra)
135
136
  cmd += ["--json", "-o", last]
136
137
  if model:
137
138
  cmd += ["-m", model]
@@ -141,6 +142,24 @@ def build_command(task: dict[str, Any], cfg: WorkerConfig, tmpdir: str) -> tuple
141
142
  raise ValueError(f"unknown runner {runner!r}")
142
143
 
143
144
 
145
+ def _sandbox_as_config(args: list[str]) -> list[str]:
146
+ """`codex exec resume` rejects -s/--sandbox, so pass the same choice as a config override."""
147
+ out: list[str] = []
148
+ i = 0
149
+ while i < len(args):
150
+ a = args[i]
151
+ if a in {"-s", "--sandbox"} and i + 1 < len(args):
152
+ out += ["-c", f'sandbox_mode="{args[i + 1]}"']
153
+ i += 2
154
+ continue
155
+ if a.startswith("--sandbox="):
156
+ out += ["-c", f'sandbox_mode="{a.split("=", 1)[1]}"']
157
+ else:
158
+ out.append(a)
159
+ i += 1
160
+ return out
161
+
162
+
144
163
  def parse_output(runner: str, stdout: str, stderr: str, tmpdir: str) -> dict[str, Any]:
145
164
  """Pull the agent's final message and session id out of the raw process output."""
146
165
  if runner == "claude":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dispatchbox
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Route events to Claude Code and Codex running on your own machines, and ask you only when it matters.
5
5
  Author: Xixian Yang
6
6
  License-Expression: MIT
@@ -93,7 +93,7 @@ Most developers already have `claude` or `codex` signed in on their workstation
93
93
 
94
94
  **Prerequisites:**
95
95
  - Python **3.10+** (uses the Python standard library only; no `pip install` required).
96
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex CLI](https://github.com/openai/codex) (`codex`) installed and signed in (`examples/hello.json` uses `claude`).
96
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex CLI](https://github.com/openai/codex) (`codex`) installed and signed in. `examples/hello.json` uses `claude`; for Codex, use [`examples/hello-codex.json`](https://github.com/XixianWasTaken/dispatchbox/blob/main/examples/hello-codex.json) in the commands below.
97
97
 
98
98
  ### 1. Create a scratch workspace and shared token
99
99
 
@@ -21,6 +21,7 @@ dispatchbox.egg-info/dependency_links.txt
21
21
  dispatchbox.egg-info/entry_points.txt
22
22
  dispatchbox.egg-info/requires.txt
23
23
  dispatchbox.egg-info/top_level.txt
24
+ examples/hello-codex.json
24
25
  examples/hello.json
25
26
  tests/__init__.py
26
27
  tests/helpers.py
@@ -0,0 +1,26 @@
1
+ {
2
+ "require_decision_for": ["delete_data"],
3
+ "defaults": { "runner": "codex", "cwd": "./demo", "timeout": "5m" },
4
+ "rules": [
5
+ {
6
+ "name": "ask",
7
+ "when": { "source": "manual", "type": "ask" },
8
+ "prompt": "Answer briefly, about the files in the current directory: {summary}",
9
+ "args": ["-s", "read-only", "--skip-git-repo-check"],
10
+ "notify": "always"
11
+ },
12
+ {
13
+ "name": "tidy",
14
+ "when": { "source": "manual", "type": "tidy" },
15
+ "prompt": "Do this in the current directory: {summary}. Deleting any file counts as delete_data.",
16
+ "args": ["-s", "read-only", "--skip-git-repo-check"]
17
+ },
18
+ {
19
+ "name": "after-decision",
20
+ "when": { "source": "dispatchbox", "type": "decision.made" },
21
+ "prompt": "The owner chose: {payload.choice}. Carry on from where you stopped.",
22
+ "args": ["-s", "workspace-write", "--skip-git-repo-check"],
23
+ "notify": "always"
24
+ }
25
+ ]
26
+ }
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dispatchbox"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Route events to Claude Code and Codex running on your own machines, and ask you only when it matters."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -130,6 +130,19 @@ class E2ETest(unittest.TestCase):
130
130
  self.assertEqual(http("GET", self.base + "/healthz", token=None)[0], 200)
131
131
  self.assertEqual(http("POST", self.base + "/events", {"source": "a"})[0], 400)
132
132
 
133
+ def test_api_errors_are_json_without_an_accept_header(self):
134
+ no_accept = {"Accept": ""}
135
+ s, body = http("POST", self.base + "/runs/999/decision", {"choice": "x"}, headers=no_accept)
136
+ self.assertEqual(s, 404)
137
+ self.assertEqual(json.loads(body), {"error": "run 999 not found"})
138
+ s, body = http("POST", self.base + "/runs/999/decision", {"choice": "x"}, token=None, headers=no_accept)
139
+ self.assertEqual(s, 401)
140
+ self.assertIn("error", json.loads(body))
141
+ # A browser posting the dashboard form still gets a page.
142
+ s, body = http("POST", self.base + "/runs/999/decision", token=None, form="choice=x",
143
+ headers={"Accept": "text/html"})
144
+ self.assertTrue(body.lstrip().startswith("<!doctype html>"), body[:80])
145
+
133
146
  def test_full_loop_with_decision_and_resume(self):
134
147
  self.start_worker()
135
148
  s, body = http("POST", self.base + "/events", {"source": "github", "type": "pr.opened", "subject": "r#1"})
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  from dispatchbox import store
10
10
  from dispatchbox.hub import Hub, extract_status
11
11
  from dispatchbox.rules import load_config
12
- from dispatchbox.worker import parse_output
12
+ from dispatchbox.worker import WorkerConfig, build_command, parse_output
13
13
  from tests.helpers import FakeClock, write_rules
14
14
 
15
15
  RULES = {
@@ -36,7 +36,7 @@ CODEX_FAILED = "\n".join([
36
36
  ])
37
37
 
38
38
 
39
- # Schema exactly as released in 0.1.0, before the usage and retry columns.
39
+ # Schema of the first pre-release version, before the usage and retry columns.
40
40
  V01_SCHEMA = """
41
41
  CREATE TABLE IF NOT EXISTS events (
42
42
  id INTEGER PRIMARY KEY,
@@ -190,6 +190,20 @@ class Step0Units(unittest.TestCase):
190
190
  self.assertTrue(bad["agent_error"])
191
191
  self.assertIn("The model is not supported", bad["output"])
192
192
 
193
+ def test_codex_resume_passes_the_sandbox_as_config(self):
194
+ cfg = WorkerConfig(hub="", name="w", token="t")
195
+ task = {"runner": "codex", "prompt": "p", "args": ["-s", "workspace-write", "--sandbox=read-only", "--skip-git-repo-check"]}
196
+ with tempfile.TemporaryDirectory() as d:
197
+ fresh, _ = build_command(task, cfg, d)
198
+ resumed, _ = build_command({**task, "resume_session": "t-1"}, cfg, d)
199
+ self.assertIn("-s", fresh) # a new session takes the flag as written
200
+ self.assertEqual(resumed[:4], ["codex", "exec", "resume", "t-1"])
201
+ self.assertNotIn("-s", resumed)
202
+ self.assertNotIn("--sandbox=read-only", resumed)
203
+ self.assertIn('sandbox_mode="workspace-write"', resumed)
204
+ self.assertIn('sandbox_mode="read-only"', resumed)
205
+ self.assertIn("--skip-git-repo-check", resumed)
206
+
193
207
  def test_migrates_old_database(self):
194
208
  with tempfile.TemporaryDirectory() as d:
195
209
  path = str(Path(d, "old.db"))
File without changes
File without changes
File without changes