Register your first agent runner and dispatch a task

Time: 15 minutes. You'll need: workspace-admin rights on the workspace, a machine reachable from Ithura at an HTTPS URL (ngrok, Tailscale funnel, or a plain public server), and a task you can hand off.

By the end you'll have a registered runner, a signed dispatch on the wire, and a completed callback that moved the run from in_progress to completed in the audit log. All from an operator terminal, no production runner code required.

What Agent Dispatch is (and is not)

Agent Dispatch is a queue with provenance. A workspace member picks a task, hands it off, and a runner your team controls does the work on infrastructure your team controls. Ithura never runs the model. Every hop is HMAC-signed and lands in the audit log.

It is not a chatbot, not autonomy-by-default, and not a hosted agent product. If that shape is not what you want, this feature is not for you.

1. Open Settings, Agent runners

From the workspace sidebar, open Settings. In the tab rail, find Agent runners (admin-only; if you don't see it, you need admin on the workspace, not just member access).

The page shows an empty list plus a Register runner button.

2. Register the runner

Click Register runner. Fill the form:

  • Name: something you'll recognise in an audit log, e.g. Claude Code on ops laptop.
  • Webhook URL: the HTTPS endpoint that will receive dispatch POSTs. For this tutorial, https://your-tunnel.ngrok.io/dispatch is enough. It must be reachable from the Ithura API, not just from your browser.
  • HMAC secret: click Generate to produce a 48-hex random value. Click the copy icon and stash it somewhere secure. Ithura never shows the value again after you click Register (the SPA cannot read the encrypted secret back).
  • Timeout: leave at 900 seconds unless you know you need longer. Cap is 3600.

Click Register. The runner appears in the list with a runner id (uuid). Copy that too; you'll need it in step 4.

3. Stand up a minimal receiver

For the tutorial, a two-endpoint Python script is enough. It verifies the incoming HMAC and immediately POSTs a completed callback so you can watch the round trip end to end.

# runner.py
import hashlib, hmac, json, os, threading, time, urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["ITHURA_RUNNER_SECRET"].encode()

def hex_sig(key: bytes, body: bytes) -> str:
    return hmac.new(key, body, hashlib.sha256).hexdigest()

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers["content-length"]))
        got = self.headers.get("X-Ithura-Signature", "")
        want = hex_sig(SECRET, body)
        if not hmac.compare_digest(got, want):
            self.send_response(401); self.end_headers(); return
        payload = json.loads(body)
        cb = payload["callback"]
        token = cb["url"].rstrip("/").split("/")[-2]  # /agent-runs/<token>/callback
        # Reply "completed" from a background thread so the outbound
        # POST returns 2xx before Ithura even reads the callback.
        threading.Thread(target=send_callback, args=(cb["url"], token, payload)).start()
        self.send_response(202); self.end_headers()

def send_callback(url, token, payload):
    time.sleep(1)
    body = json.dumps({
        "status": "completed",
        "result_url": "https://example.com/pr/1",
        "result_summary": "Tutorial run: pretended to ship a PR",
    }).encode()
    sig = hex_sig(SECRET + token.encode(), body)
    req = urllib.request.Request(url, data=body, method="POST", headers={
        "Content-Type": "application/json",
        "X-Ithura-Signature": sig,
    })
    urllib.request.urlopen(req).read()

HTTPServer(("0.0.0.0", 8787), H).serve_forever()

Run it with your generated secret:

ITHURA_RUNNER_SECRET=<the 48-hex you stashed> python3 runner.py

Expose it: ngrok http 8787 (or the tunnel of your choice) and paste the resulting HTTPS URL back into the runner's Webhook URL field (edit in the runner list if you need to change it).

4. Dispatch a task

v1 is API-first. From any terminal with a bearer token for a workspace member+, dispatch a task:

curl -sS -X POST \
  "$ITHURA_API/workspaces/$SLUG/projects/$PROJECT_ID/issues/$ISSUE_ID/dispatch-agent/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "runner_id": "<the runner uuid from step 2>",
    "acceptance_criteria": "Ship a PR that closes this. Tests must be green."
  }'

The response returns the run id and status: queued. Within a second or two you should see:

  • The runner script prints the incoming request (with a valid signature).
  • Ithura's audit log adds workspace.agent_dispatch.queued, then .in_progress, then .completed.
  • The task's Agent runs section shows the run with a link to https://example.com/pr/1 and the summary "Tutorial run: pretended to ship a PR".

5. Read the audit trail

From the workspace sidebar, open Settings, Audit log. Filter by workspace.agent_dispatch. You'll see the three transitions, who dispatched, which runner, and the terminal payload the runner posted back. Enterprise workspaces can stream the same trail out as CSV; see Audit CSV export.

What next

  • Agent Dispatch Queue: the full contract including intermediate in_progress callbacks, the runner-side signature verification recipe, and the follow-ups on the roadmap (UI dispatch action, auto-dispatch rules, cost tracking, retry-in-place).
  • Sovereign: every dispatch runs on your own infrastructure; Ithura is the tracker and the audit surface.
  • Workspace webhooks: the outbound signing pattern here mirrors the workspace-webhooks HMAC scheme, so a runner and a webhook subscriber can share the same verification code.