Skip to content
Merged
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
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,129 @@ axctl auth whoami --json
The generated agent profile/config is what Claude Code Channel, headless MCP,
MCP Jam, and long-running agents should use.

## Gateway MVP

`ax gateway` is the first local control-plane runtime for ax-cli. It keeps the
bootstrap user PAT in one trusted local place, mints agent PATs on demand, and
supervises managed runtimes so the child process never needs the raw PAT or JWT.

The first slice starts headless, but it now includes a live terminal operator
view:

```bash
# 1. Store the Gateway bootstrap login
ax gateway login

# 2. Register a managed echo bot
ax gateway agents add echo-bot --type echo

# 3. Run the local Gateway supervisor
ax gateway run
```

In another shell or device:

```bash
ax send --to echo-bot "ping" --no-wait
ax gateway status
ax gateway watch
ax gateway ui
ax gateway agents show echo-bot
```

`ax gateway status` now shows recent control-plane activity as well as managed
runtime state, and Gateway-authored replies carry control-plane metadata so
operator attribution is less ambiguous during dogfooding.

Gateway setup should be treated as an agent-operable workflow, not a browser-
only wizard. The repo skill for that flow is
[`skills/gateway-agent-setup/SKILL.md`](skills/gateway-agent-setup/SKILL.md),
which wraps add/update/doctor/approval work on top of the local Gateway.

`ax gateway watch` is the first dashboard-style operator surface: a live
terminal view with Gateway health, alerts, fleet counts, managed-agent roster,
and recent control-plane events. `ax gateway agents show <name>` gives the
first drill-in view for one managed runtime, and `ax gateway agents test <name>`
sends a Gateway-authored smoke test to that managed agent.

`ax gateway ui` serves the same local Gateway state over a small local web
dashboard on `127.0.0.1` by default. It now speaks the same local Gateway
contract as the CLI for status, agent drill-in, add/start/stop/remove, and
managed test sends, so the browser UI and terminal UI stay aligned to one
control-plane source of truth while the product shape is still forming.

`ax gateway templates` exposes the same user-facing agent catalog the web UI
uses, including what each agent type needs and what kind of delivery, liveness,
activity, and tool telemetry the operator should expect.

The primary agent types start with:

- `Echo (Test)` — built-in ping/echo bot for proving the control plane.
- `Ollama` — a local-model runtime managed through Gateway.
- `Hermes Sentinel` — a Gateway-managed long-running Hermes coding agent.
- `Claude Code Channel` — an attached Claude Code session managed and observed by Gateway.

The lower-level runtime backends still exist under `ax gateway runtime-types`
for advanced/debug use, but they are not the main operator-facing choices.
Advanced users can still override launch commands and working directories from
the CLI or the UI's advanced launch section when they are building a custom
bridge.

See [Gateway Agent Runtimes](docs/gateway-agent-runtimes.md) for the operating
model. The key migration from the original CLI setup is management, not a new
agent brain: Gateway owns credentials, launch specs, lifecycle, status, and
liveness, while Hermes sentinels and Claude Code channels keep the runtime
patterns that already worked.

Managed `exec` runtimes can now emit structured progress lines back to Gateway
while they are still working. The bridge prints lines prefixed with
`AX_GATEWAY_EVENT ` and Gateway turns them into control-plane activity,
`agent_processing`, and tool-call audit notifications.

Example: connect the local Codex CLI as a managed runtime for `@codex`:

```bash
ax gateway agents add codex \
--type exec \
--exec "python3 examples/codex_gateway/codex_bridge.py" \
--workdir /absolute/path/to/ax-cli
```

Example: add a second connected sender identity and use it to message `@codex`
without creating an agent-to-agent reply loop:

```bash
ax gateway agents add codex-gateway-inbox --type inbox
ax gateway agents send codex-gateway-inbox "pause for 8 seconds and narrate activity" --to codex
```

Example: update a managed runtime without recreating its identity:

```bash
ax gateway agents update northstar --template hermes
ax gateway agents doctor northstar
```

Gateway test sends now default to an agent-authored path using a passive
Gateway-managed sender identity. For diagnostics, you can still force a
user-authored test explicitly:

```bash
ax gateway agents test northstar
ax gateway agents test northstar --author user
```

For alert-style or scheduled custom payloads, use the normal send path instead
of the test button:

```bash
ax gateway agents send switchboard-12d6eafd "Cron job: nightly sync finished" --to northstar
```

This is a compatibility-first Gateway: today it still uses agent PATs against
the existing platform APIs, but the Gateway owns those credentials centrally so
managed runtimes do not.

## Claude Code Channel — Connect from Anywhere

**The first multi-agent channel for Claude Code.** Send a message from your phone, Claude Code receives it in real-time, delegates work to specialist agents, and reports back.
Expand Down Expand Up @@ -483,6 +606,14 @@ returned messages have actually been handled.
| Command | Description |
|---------|-------------|
| `axctl login` | Set up or refresh the user login token without touching agent config |
| `ax gateway login` | Store the local Gateway bootstrap session |
| `ax gateway status` | Show Gateway daemon + managed runtime status |
| `ax gateway agents test NAME` | Send a Gateway-authored smoke test to one managed agent |
| `ax gateway templates` | List the main Gateway agent types users can add |
| `ax gateway runtime-types` | List advanced/internal runtime backends |
| `ax gateway ui` | Serve the local Gateway web dashboard |
| `ax gateway agents show NAME` | Drill into one managed agent |
| `ax gateway agents send NAME "msg" --to codex` | Send as a managed agent identity |
| `ax auth whoami` | Current identity + profile + fingerprint |
| `ax agents list` | List agents in the space |
| `ax spaces list` | List spaces you belong to |
Expand All @@ -496,6 +627,10 @@ returned messages have actually been handled.
| Command | Description |
|---------|-------------|
| `ax events stream` | Raw SSE event stream |
| `ax gateway run` | Run the local Gateway supervisor |
| `ax gateway watch` | Live Gateway dashboard in the terminal |
| `ax gateway ui --port 8765` | Local browser dashboard over Gateway state |
| `ax gateway agents add NAME --template hermes` | Add a Hermes-managed agent using the default bridge |
| `ax listen --exec "./bot"` | Listen for @mentions with handler |
| `ax watch --mention` | Block until condition matches on SSE |

Expand Down
75 changes: 75 additions & 0 deletions ax_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@ def set_agent_processing_status(
*,
agent_name: str | None = None,
space_id: str | None = None,
activity: str | None = None,
tool_name: str | None = None,
progress: dict | None = None,
detail: dict | None = None,
reason: str | None = None,
error_message: str | None = None,
retry_after_seconds: int | None = None,
parent_message_id: str | None = None,
) -> dict:
"""POST /api/v1/agents/processing-status.

Expand All @@ -446,13 +454,80 @@ def set_agent_processing_status(
body: dict = {"message_id": message_id, "status": status}
if agent_name:
body["agent_name"] = agent_name
optional_fields = {
"activity": activity,
"tool_name": tool_name,
"progress": progress,
"detail": detail,
"reason": reason,
"error_message": error_message,
"retry_after_seconds": retry_after_seconds,
"parent_message_id": parent_message_id,
}
for key, value in optional_fields.items():
if value is not None:
body[key] = value
headers = self._with_agent(self.agent_id)
if space_id:
headers["X-Space-Id"] = space_id
r = self._http.post("/api/v1/agents/processing-status", json=body, headers=headers)
r.raise_for_status()
return self._parse_json(r)

def record_tool_call(
self,
*,
tool_name: str,
tool_call_id: str,
space_id: str | None = None,
tool_action: str | None = None,
resource_uri: str | None = None,
arguments_hash: str | None = None,
kind: str | None = None,
arguments: dict | None = None,
initial_data: dict | None = None,
status: str = "success",
duration_ms: int | None = None,
agent_name: str | None = None,
agent_id: str | None = None,
message_id: str | None = None,
correlation_id: str | None = None,
) -> dict:
"""POST /api/v1/tool-calls.

Records a tool-call audit event from an authenticated agent runtime.
The backend stores it durably and fans out progress/tool-call SSE so
the operator UI can show richer in-flight activity.
"""
body: dict = {
"tool_name": tool_name,
"tool_call_id": tool_call_id,
"status": status,
}
optional_fields = {
"space_id": space_id,
"tool_action": tool_action,
"resource_uri": resource_uri,
"arguments_hash": arguments_hash,
"kind": kind,
"arguments": arguments,
"initial_data": initial_data,
"duration_ms": duration_ms,
"agent_name": agent_name,
"agent_id": agent_id,
"message_id": message_id,
"correlation_id": correlation_id,
}
for key, value in optional_fields.items():
if value is not None:
body[key] = value
headers = self._with_agent(agent_id)
if space_id:
headers["X-Space-Id"] = space_id
r = self._http.post("/api/v1/tool-calls", json=body, headers=headers)
r.raise_for_status()
return self._parse_json(r)

def upload_file(self, file_path: str, *, space_id: str | None = None) -> dict:
"""POST /api/v1/uploads — upload a local file.

Expand Down
6 changes: 6 additions & 0 deletions ax_cli/commands/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""ax auth — identity and token management."""

import os
from pathlib import Path

import httpx
Expand Down Expand Up @@ -192,6 +193,8 @@ def doctor(
console.print(f" space_id = {effective.get('space_id')} ({effective.get('space_source')})")
console.print(f" agent_name = {effective.get('agent_name')} ({effective.get('agent_name_source')})")
console.print(f" agent_id = {effective.get('agent_id')} ({effective.get('agent_id_source')})")
if data.get("runtime_config"):
console.print(f" runtime_config = {data['runtime_config']}")
if data.get("selected_env"):
console.print(f" selected_env = {data['selected_env']}")
if data.get("selected_profile"):
Expand Down Expand Up @@ -235,6 +238,9 @@ def whoami(as_json: bool = JSON_OPTION):
local = _local_config_dir()
if local and (local / "config.toml").exists():
data["local_config"] = str(local / "config.toml")
runtime_config = os.environ.get("AX_CONFIG_FILE")
if runtime_config:
data["runtime_config"] = runtime_config

if as_json:
print_json(data)
Expand Down
Loading