# ARGUS-3 — Channels
> Part of the ARGUS documentation set (`argus/docs/`):
> [architecture](./architecture.md) · [security-warden](./security-warden.md) · [economy-integration](./economy-integration.md) · [token-economy](./token-economy.md) · [autonomy](./autonomy.md) · **channels**
ARGUS is **one bounded agent core with many mouths.** The same plan → execute →
observe loop — governed by WARDEN 🛡️, the budget governor, memory, and the
provider router — answers a developer on the CLI, you on Telegram, a web
frontend over HTTP, and another agent over MCP. The core does not know or care
which channel a task arrived on; it only knows the **approval policy** that came
attached to it.
The deliberate choice here: **each channel owns the auth/owner model that is
natural to it.** A one-size-fits-all auth scheme would be either too weak for a
public HTTP endpoint or too heavy for a local CLI. So the CLI trusts the local
user, Telegram claims an owner on first contact, HTTP carries a bearer token,
and MCP rides the host's local stdio trust boundary. The agent core stays
identical; only the gate in front of it changes.
```mermaid
flowchart TB
subgraph CH["Channels — each owns its auth model"]
CLI["CLI
local user"]
TG["Telegram
owner-lock (TOFU)"]
HTTP["HTTP API
bearer token"]
MCP["MCP-server
local / stdio"]
DISCORD["Discord
owner id"]
SLACK["Slack
owner id"]
EMAIL["Email
allow-listed from"]
MATRIX["Matrix
owner id"]
MORE["…"]
end
CLI --> AUTH
TG --> AUTH
HTTP --> AUTH
MCP --> AUTH
DISCORD --> AUTH
SLACK --> AUTH
EMAIL --> AUTH
MATRIX --> AUTH
MORE --> AUTH
AUTH{{"per-channel auth
+ approval policy"}}
AUTH --> HOST["ChannelHost
normalise → task + approve()"]
HOST --> CORE
subgraph CORE["Agent core — channel-agnostic"]
direction LR
WARDEN["🛡️ WARDEN"]
BUDGET["budget governor"]
MEM["memory"]
PROV["providers"]
end
```
Every channel funnels into the same `ChannelHost`, which normalises the inbound
message into a `task` string plus an `approve()` callback, then calls
`Agent.run(task, { approve })`. The approval callback is where a channel's
character lives: interactive channels can prompt the human; non-interactive ones
deny-by-default. WARDEN vets every MCP tool **regardless of channel** — the
channel decides *who may ask*, WARDEN decides *what may run*.
---
## Channel matrix
| Channel | Status | Owner / Auth model | External deps | Best for | Ecosystem fit |
|---|---|---|---|---|---|
| **CLI** | SHIPPED | Local user (interactive approval at the terminal) | none | dev / scripts / cron | n/a |
| **Telegram** | SHIPPED | Owner-lock, trust-on-first-use — first `/start` claims the bot (`ARGUS_TELEGRAM_OWNER_ID` overrides), persisted; sensitive tools need an in-chat `/yes` | bot token | personal mobile assistant | high |
| **HTTP API** | SHIPPED *(this release)* | `GET /health` & `GET /status` are open (no secret); `POST /ask` requires `Bearer ARGUS_HTTP_TOKEN`; sensitive tools default-deny | none (built-in `node:http`) | automation, web frontends, health monitoring; the substrate for voice/web | high — `/health` is how ARGUS appears as a node to Alien Monitor 👽 |
| **MCP-server** | SHIPPED *(this release)* | Local / stdio; exposes tools `argus_ask` & `argus_status`; sensitive tools default-deny | MCP client (Claude Desktop, Cursor, other agents) | being **used as a tool** by other agents / IDEs; the economy provider role | highest — this is how ARGUS sells its capability into the mesh / hub 🛒 |
| **Discord** | PLANNED | Owner-lock by Discord user id | bot token | communities / personal | high |
| **Slack** | PLANNED | Owner-lock by Slack user id (Socket Mode) | app token | work / teams | high |
| **Email (IMAP/SMTP)** | PLANNED | Allow-listed from-address | mailbox creds | async, universal, no platform lock-in | medium |
| **Matrix** | PLANNED | Owner-lock by Matrix id | homeserver creds | decentralized / privacy (fits the self-hosted ethos) | high |
| **WhatsApp** | PLANNED | Allow-listed number (Cloud API) | Meta app | mass reach | medium |
| **Voice** (Telegram voice notes → STT, or Twilio phone) | PLANNED | Rides Telegram / HTTP | STT provider | hands-free | medium |
| **Web chat widget** | PLANNED | Rides HTTP `/ask` + token | none (reuse `aimarket-widget`) | embedding in sites | medium |
| **Economy / Mesh** (ARGUS invoked as a paid capability) | PLANNED | Escrow-paid invoke via the Hub | wallet | being hired by other agents | highest — the native demand↔supply loop |
The PLANNED channels are not speculative re-architectures: each is just another
adapter that satisfies the same `receive → Agent.run → reply` contract (see
[Add a channel](#add-a-channel)). The two channels shipping this release —
**HTTP API** and **MCP-server** — are detailed next.
---
## The two new channels in detail
### HTTP API 🛒
A minimal HTTP surface built on the standard library (`node:http` — no framework
dependency). It splits cleanly into an **open observability plane** and a
**gated work plane**.
| Method & path | Auth | Purpose |
|---|---|---|
| `GET /health` | open (no secret) | liveness + node identity — the monitor visibility hook |
| `GET /status` | open (no secret) | richer state (budget meter, economy on/off, configured channels) |
| `POST /ask` | `Bearer ARGUS_HTTP_TOKEN` | run a task through the agent core |
`GET /health` returns a compact, stable JSON shape:
```json
{
"status": "ok",
"agent": "argus",
"version": "0.1.0",
"model": "claude-sonnet",
"economy": "off",
"uptimeSec": 1042
}
```
`POST /ask` takes `{"task": "..."}` and returns the answer alongside the budget
meter and the run outcome:
```json
{
"answer": "…",
"meter": { "tokensIn": 1280, "tokensOut": 412, "usd": 0.0041 },
"outcome": "completed"
}
```
**Configuration.** The HTTP channel is driven by `config.http { enabled, port }`
in `argus.config.json`, with environment overrides:
- `ARGUS_HTTP_PORT` — listen port (default **8787**)
- `ARGUS_HTTP_TOKEN` — the bearer secret required by `POST /ask` (lives in `.env`, never committed)
If `ARGUS_HTTP_TOKEN` is unset, `POST /ask` is refused outright — the work plane
fails closed rather than serving an unauthenticated agent. The observability
plane (`/health`, `/status`) carries no secret by design: it exposes only
non-sensitive liveness data.
**Examples.**
```bash
# Open — no auth. This is what a monitor polls.
curl -s http://127.0.0.1:8787/health
# Gated — bearer token required.
curl -s http://127.0.0.1:8787/ask \
-H "Authorization: Bearer $ARGUS_HTTP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"summarise https://example.com in three bullets"}'
```
**Why `/health` matters.** It is ARGUS's **node-visibility hook**. The open,
secret-free `/health` endpoint is exactly the shape Alien Monitor 👽 polls to
discover and render a node on the network map. By shipping it, an ARGUS instance
stops being a private client and becomes a *visible participant* in the
ecosystem — observable without ever exposing the ability to run tasks. The HTTP
channel is also the **substrate** the planned voice and web-widget channels ride
on: both terminate in a `POST /ask`.
### MCP-server mode 🔮
```bash
argus mcp
```
This runs ARGUS itself as a **stdio MCP server**, exposing two tools to any MCP
client:
- `argus_ask({ task })` — run a task through the full agent core and return the answer.
- `argus_status()` — report budget meter, model, and economy state.
The trust boundary is the local stdio pipe: the MCP host launched the process, so
the caller is the local user or an agent the user already trusts. Sensitive tools
remain **deny-by-default** on this non-interactive channel — there is no human to
prompt, so WARDEN's sensitive-tool gate refuses rather than guesses.
A Claude Desktop / generic MCP client config snippet:
```json
{
"mcpServers": {
"argus": {
"command": "node",
"args": ["dist/index.js", "mcp"]
}
}
}
```
**Why this is the most important new channel.** MCP-server mode is what makes
ARGUS **composable** — another agent, an IDE, or a desktop assistant can call
`argus_ask` the same way ARGUS itself calls any other MCP tool. This is the
**provider / "sell capability" path**: it is the mechanism by which ARGUS's
capability is offered *into* the mesh and the Hub 🛒, the supply side of the
demand↔supply loop. The planned **Economy / Mesh** channel is this same provider
role with escrow-paid invocation layered on top (see
[economy-integration.md](./economy-integration.md)).
---
## Running channels
| Command | What it runs |
|---|---|
| `argus serve` | Telegram (if a bot token is set) **and** the HTTP API together, in one process |
| `argus mcp` | the MCP stdio server (one server, speaking to its host over stdin/stdout) |
| `docker compose up` | the container's default — runs `serve` |
```mermaid
flowchart LR
SERVE["argus serve"] --> TG2["Telegram channel
(if token)"]
SERVE --> HTTP2["HTTP API channel"]
MCPCMD["argus mcp"] --> MCP2["MCP stdio channel"]
TG2 --> CORE2
HTTP2 --> CORE2
MCP2 --> CORE2
CLI2["argus ask / chat"] --> CORE2
CORE2["one bounded agent core
🛡️ WARDEN · budget · memory · providers"]
```
**One bounded agent core is shared** across whatever is running. `serve`
multiplexes Telegram and HTTP onto the same in-process core; `mcp` exposes that
same core over stdio. There is no per-channel agent, no duplicated budget
governor, no second memory store — just different front doors. **Each task
carries its channel's approval policy**, so an HTTP `POST /ask` runs with
deny-by-default sensitive tools while the same task typed into `argus chat` can
prompt you interactively.
For container deployment (`docker compose up` → `serve`), see the Deployment
note.
---
## Security note 🛡️
Channel security rests on a clear separation of concerns:
- **Owner-gating is per-channel and baked in.** Each adapter is responsible for
proving *who* is talking — the CLI trusts the local user, Telegram owner-locks
on first `/start`, HTTP requires the bearer token, MCP relies on the local
stdio boundary. There is no global, channel-agnostic auth; each channel uses
the model appropriate to its threat surface.
- **WARDEN vets every MCP tool regardless of channel.** Authentication answers
*who may ask*; WARDEN answers *what may run*. The static → threat → reputation
→ pinning gate chain runs identically whether the task came from the terminal
or a remote HTTP caller. Owning the channel never buys a pass through the
firewall.
- **Sensitive tools are deny-by-default on non-interactive channels.** On HTTP
and MCP there is no human in the loop, so write/delete/exec/payment-class tools
are refused rather than auto-approved. On **interactive** channels (Telegram,
CLI) the same tools require explicit confirmation — an in-chat `/yes` on
Telegram, a terminal prompt on the CLI — before they run.
The result: a public HTTP endpoint or a shared MCP server can be useful without
being dangerous. The most powerful tools simply are not reachable from a channel
that cannot obtain real-time human consent.
---
## Add a channel
Adding a channel is small and self-contained. An adapter does three things:
1. **`receive`** — accept the platform's inbound event and normalise it into a
`task` string (plus any context).
2. **`Agent.run(task, { approve })`** — call the shared agent core, passing an
`approve()` callback that encodes this channel's approval policy
(interactive prompt, or deny-by-default for non-interactive surfaces).
3. **`reply`** — render the agent's answer back into the platform's format.
```mermaid
flowchart LR
IN["platform event"] --> RECV["receive()
→ task"]
RECV --> AUTHN{"channel auth
owner / token / allow-list"}
AUTHN -- "reject" --> DROP["ignore / 401"]
AUTHN -- "accept" --> RUN["Agent.run(task, { approve })"]
RUN --> REPLY["reply()
render answer"]
```
**The auth model is the adapter's responsibility** — it decides whether to
owner-lock, check a bearer token, or allow-list an address, and it supplies the
`approve()` policy. Everything past `Agent.run` — WARDEN, the budget governor,
memory, provider routing — is inherited unchanged from the one bounded core. A
new channel adds a front door; it never forks the agent.
---
> Following the ecosystem's tri-lingual convention, Russian and Spanish
> companion docs (`channels-ru.md` / `channels-es.md`) will follow.