From 056fe83edf19c5f6ed743377b89aa432165c9eef Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 2 Feb 2026 21:19:50 +0530 Subject: [PATCH] fix(agents): give agents executable curl instructions + shared base URL helper --- backend/app/api/org.py | 51 +++++------------------------- backend/app/core/urls.py | 35 ++++++++++++++++++++ backend/app/integrations/notify.py | 21 +++++++++--- 3 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 backend/app/core/urls.py diff --git a/backend/app/api/org.py b/backend/app/api/org.py index ee7127b8..618dd758 100644 --- a/backend/app/api/org.py +++ b/backend/app/api/org.py @@ -5,6 +5,7 @@ from sqlalchemy.exc import IntegrityError from sqlmodel import Session, select from app.api.utils import get_actor_employee_id, log_activity +from app.core.urls import public_api_base_url from app.db.session import get_session from app.integrations.openclaw import OpenClawClient from app.models.org import Department, Employee, Team @@ -20,48 +21,6 @@ from app.schemas.org import ( router = APIRouter(tags=["org"]) -def _public_api_base_url() -> str: - """Return a LAN-reachable base URL for the Mission Control API. - - - Priority: - 1) MISSION_CONTROL_BASE_URL env var (recommended) - 2) First non-loopback IPv4 from `hostname -I` - - - Never returns localhost/ because agents may run on another machine.""" - - import os - import re - import subprocess - - explicit = os.environ.get("MISSION_CONTROL_BASE_URL") - if explicit: - return explicit.rstrip("/") - - try: - out = subprocess.check_output(["bash", "-lc", "hostname -I"], text=True).strip() - # pick first RFC1918-ish IPv4, skip docker/loopback - ips = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", out) - for ip in ips: - if ip.startswith("127."): - continue - if ip.startswith("172.17."): - continue - if ( - ip.startswith("192.168.") - or ip.startswith("10.") - or ip.startswith("172.16.") - or ip.startswith("172.") - ): - return f"http://{ip}:8000" - except Exception: - pass - - # Fallback placeholder (should be overridden by env var) - return "http://:8000" - - def _default_agent_prompt(emp: Employee) -> str: """Generate a conservative default prompt for a newly-created agent employee. @@ -76,9 +35,15 @@ def _default_agent_prompt(emp: Employee) -> str: f"Your employee_id is {emp.id}.\n" f"Title: {title}. Department id: {dept}.\n\n" "Mission Control API access (no UI):\n" - f"- Base URL: {_public_api_base_url()}\n" + f"- Base URL: {public_api_base_url()}\n" "- Auth: none. REQUIRED header on ALL write operations: X-Actor-Employee-Id: \n" f" Example for you: X-Actor-Employee-Id: {emp.id}\n\n" + "How to execute writes from an OpenClaw agent (IMPORTANT):\n" + "- Use the exec tool to run curl against the Base URL above.\n" + "- Example: start a task\n" + " curl -sS -X PATCH $BASE/tasks/ -H 'X-Actor-Employee-Id: ' -H 'Content-Type: application/json' -d '{\"status\":\"in_progress\"}'\n" + "- Example: add a progress comment\n" + " curl -sS -X POST $BASE/task-comments -H 'X-Actor-Employee-Id: ' -H 'Content-Type: application/json' -d '{\"task_id\":,\"body\":\"...\"}'\n\n" "Common endpoints (JSON):\n" "- GET /tasks, POST /tasks\n" "- GET /task-comments, POST /task-comments\n" diff --git a/backend/app/core/urls.py b/backend/app/core/urls.py new file mode 100644 index 00000000..f0cd8527 --- /dev/null +++ b/backend/app/core/urls.py @@ -0,0 +1,35 @@ +from __future__ import annotations + + +def public_api_base_url() -> str: + """Return a LAN-reachable base URL for the Mission Control API. + + Priority: + 1) MISSION_CONTROL_BASE_URL env var (recommended) + 2) First non-loopback IPv4 from `hostname -I` + + Never returns localhost because agents may run on another machine. + """ + + import os + import re + import subprocess + + explicit = os.environ.get("MISSION_CONTROL_BASE_URL") + if explicit: + return explicit.rstrip("/") + + try: + out = subprocess.check_output(["bash", "-lc", "hostname -I"], text=True).strip() + ips = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", out) + for ip in ips: + if ip.startswith("127."): + continue + if ip.startswith("172.17."): + continue + if ip.startswith(("192.168.", "10.", "172.")): + return f"http://{ip}:8000" + except Exception: + pass + + return "http://:8000" diff --git a/backend/app/integrations/notify.py b/backend/app/integrations/notify.py index e56e0e03..de43e8ce 100644 --- a/backend/app/integrations/notify.py +++ b/backend/app/integrations/notify.py @@ -101,14 +101,25 @@ def build_message(ctx: NotifyContext, recipient: Employee) -> str: desc_block = f"\n\nDescription:\n{desc}" if desc else "" # Keep this deterministic: agents already have base URL + header guidance in their prompt. + base_url = __import__( + "app.core.urls", fromlist=["public_api_base_url"] + ).public_api_base_url() + return ( f"{base}\n\n" - "You are the assignee. Start NOW:\n" - f"1) PATCH /tasks/{t.id} → status=in_progress (use X-Actor-Employee-Id: {recipient.id})\n" - f"2) POST /task-comments → task_id={t.id} with a 1-2 line plan + next action\n" + f"Set BASE={base_url}\n\n" + "You are the assignee. Start NOW (use the exec tool to run these curl commands):\n" + f"1) curl -sS -X PATCH $BASE/tasks/{t.id} -H 'X-Actor-Employee-Id: {recipient.id}' " + "-H 'Content-Type: application/json' -d '{\"status\":\"in_progress\"}'\n" + f"2) curl -sS -X POST $BASE/task-comments -H 'X-Actor-Employee-Id: {recipient.id}' " + f'-H \'Content-Type: application/json\' -d \'{{"task_id":{t.id},"body":"Plan: ... Next: ..."}}\'\n' "3) Do the work\n" - "4) POST /task-comments → progress updates\n" - f"5) When complete: PATCH /tasks/{t.id} → status=done and post a final summary comment" + f"4) Post progress updates via POST $BASE/task-comments (same headers)\n" + f"5) When complete: curl -sS -X PATCH $BASE/tasks/{t.id} -H 'X-Actor-Employee-Id: {recipient.id}' " + "-H 'Content-Type: application/json' -d '{\"status\":\"done\"}' and post a final summary comment" + f"-H 'X-Actor-Employee-Id: {recipient.id}' " + "-H 'Content-Type: application/json' " + '-d \'{"status":"done"}\' and post a final summary comment' f"{desc_block}" )