Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ The installation was successful if you see help output after you run `agentex -h

## Setup

> **OSS contributors:** Run `python scripts/agentex_dev_doctor.py` before `./dev.sh` to catch Docker/port/ECR issues ([#163](https://github.com/scaleapi/scale-agentex/issues/163)). See [docs/LOCAL_DEV.md](docs/LOCAL_DEV.md).

Before you share your agents with other people (or your company), you'll want a fully isolated developer sandbox. This allows you to freely develop agents without affecting anyone else or be affected by anyone else.

To do this, you just need to spin up the [Agentex Server](https://github.com/scaleapi/scale-agentex/tree/main/agentex) and a [Developer UI](https://github.com/scaleapi/scale-agentex/tree/main/agentex-ui) which allows you to interact with your agent nicely. This way you'll know how your agent feels in a simple UI.
Expand Down
43 changes: 43 additions & 0 deletions agentex/tests/unit/scripts/test_dev_doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Tests for scripts/agentex_dev_doctor.py"""

from __future__ import annotations

import importlib.util
import sys
import unittest
from pathlib import Path
from unittest import mock

_MODULE_PATH = Path(__file__).resolve().parents[4] / "scripts" / "agentex_dev_doctor.py"


def _load_module():
spec = importlib.util.spec_from_file_location("agentex_dev_doctor", _MODULE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules["agentex_dev_doctor"] = module
spec.loader.exec_module(module)
return module


agentex_dev_doctor = _load_module()

Comment thread
greptile-apps[bot] marked this conversation as resolved.

class TestAgentexDevDoctor(unittest.TestCase):
def test_registry_unset_passes(self):
with mock.patch.dict("os.environ", {}, clear=True):
gate = agentex_dev_doctor._check_docker_registry_env()
self.assertTrue(gate["passed"])

def test_private_ecr_fails(self):
env = {"DOCKER_REGISTRY": "022465994601.dkr.ecr.us-west-2.amazonaws.com/golden/"}
with mock.patch.dict("os.environ", env, clear=True):
gate = agentex_dev_doctor._check_docker_registry_env()
self.assertFalse(gate["passed"])

def test_port_free_helper(self):
self.assertIsInstance(agentex_dev_doctor._port_free(59999), bool)


if __name__ == "__main__":
unittest.main()
34 changes: 34 additions & 0 deletions docs/LOCAL_DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Local development (OSS contributors)

Agentex local dev uses **public** Docker Hub base images by default. The `agentex/Dockerfile`
accepts an optional `DOCKER_REGISTRY` build arg — leave it **unset** for open-source onboarding.

## Quick preflight

Before `./dev.sh` or `make dev`:

```bash
python scripts/agentex_dev_doctor.py
```

Checks: `docker`, `uv`, daemon reachable, `agentex/docker-compose.yml` present, critical ports
free, and `DOCKER_REGISTRY` not pointing at private ECR ([#163](https://github.com/scaleapi/scale-agentex/issues/163)).

## Common failures

| Symptom | Fix |
|---------|-----|
| `401 Unauthorized` pulling `*.amazonaws.com` base image | `unset DOCKER_REGISTRY` and rebuild |
| Redis port conflict on `6379` | `brew services stop redis` (macOS) |
| Postgres port conflict on `5432` | Stop local Postgres or change host mapping |
| Docker not running | Start Docker Desktop |

## Start stack

```bash
./dev.sh # macOS/Linux orchestration
# or
cd agentex && make dev
```

The compose file builds `agentex/Dockerfile` target `dev` without a private registry prefix.
147 changes: 147 additions & 0 deletions scripts/agentex_dev_doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Preflight checks before Agentex local development (./dev.sh / make dev).

Addresses onboarding failures when Docker, uv, or port conflicts block OSS contributors
(see scaleapi/scale-agentex#163).

Usage:
python scripts/agentex_dev_doctor.py
python scripts/agentex_dev_doctor.py --json
"""

from __future__ import annotations

import argparse
import json
import os
import shutil
import socket
import subprocess
import sys
from pathlib import Path
from typing import Any

REPO_ROOT = Path(__file__).resolve().parents[1]
COMPOSE_FILE = REPO_ROOT / "agentex" / "docker-compose.yml"
PRIVATE_ECR_MARKERS = ("amazonaws.com", "dkr.ecr.")


def _check_cmd(name: str) -> dict[str, Any]:
path = shutil.which(name)
return {
"name": f"cmd_{name}",
"passed": path is not None,
"detail": path or f"{name} not found on PATH",
}


def _check_docker_daemon() -> dict[str, Any]:
try:
subprocess.run(
["docker", "info"],
check=True,
capture_output=True,
text=True,
timeout=15,
)
return {"name": "docker_daemon", "passed": True, "detail": "docker daemon reachable"}
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
return {"name": "docker_daemon", "passed": False, "detail": f"docker info failed: {exc}"}


def _port_free(port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.bind(("127.0.0.1", port))
return True
except OSError:
return False


Comment thread
greptile-apps[bot] marked this conversation as resolved.
def _check_ports() -> list[dict[str, Any]]:
ports = {
6379: "redis (stop local redis: brew services stop redis)",
5432: "postgres",
5003: "agentex API",
}
gates = []
for port, label in ports.items():
ok = _port_free(port)
gates.append(
{
"name": f"port_{port}",
"passed": ok,
"detail": f"{label} port {port} available" if ok else f"{label} port {port} in use",
}
)
return gates


def _check_docker_registry_env() -> dict[str, Any]:
reg = os.environ.get("DOCKER_REGISTRY", "")
if not reg:
return {
"name": "docker_registry",
"passed": True,
"detail": "DOCKER_REGISTRY unset — public python/node base images used",
}
if any(marker in reg for marker in PRIVATE_ECR_MARKERS):
return {
"name": "docker_registry",
"passed": False,
"detail": (
f"DOCKER_REGISTRY={reg!r} points at private ECR; "
"unset for OSS local dev (see docs/LOCAL_DEV.md)"
),
}
return {
"name": "docker_registry",
"passed": True,
"detail": f"DOCKER_REGISTRY={reg!r}",
"warn": True,
}


def _check_compose_file() -> dict[str, Any]:
ok = COMPOSE_FILE.is_file()
return {
"name": "compose_file",
"passed": ok,
"detail": str(COMPOSE_FILE) if ok else "agentex/docker-compose.yml missing",
}


def run_doctor() -> dict[str, Any]:
gates: list[dict[str, Any]] = [
_check_cmd("docker"),
_check_cmd("uv"),
_check_docker_daemon(),
_check_compose_file(),
_check_docker_registry_env(),
* _check_ports(),
]
active = [g for g in gates if not g.get("warn")]
passed = all(g["passed"] for g in active)
return {"repo": str(REPO_ROOT), "gates": gates, "passed": passed}


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Agentex local dev preflight")
parser.add_argument("--json", action="store_true")
args = parser.parse_args(argv)

report = run_doctor()
if args.json:
print(json.dumps(report, indent=2))
else:
print(f"Agentex dev doctor — {'PASS' if report['passed'] else 'FAIL'}")
for gate in report["gates"]:
mark = "PASS" if gate["passed"] else "FAIL"
if gate.get("warn"):
mark = "WARN"
print(f" [{mark}] {gate['name']}: {gate['detail']}")
return 0 if report["passed"] else 1


if __name__ == "__main__":
raise SystemExit(main())