Skip to main content

README.md

# Legatus

> **JSON-RPC boundary: STDIO ↔ HTTP / WebSocket**

**Legatus** is a protocol boundary process.  
It reads JSON-RPC from STDIN and relays messages through one of two modes:

- `:http` — request/response transport to upstream HTTP endpoint
- `:websocket` — bidirectional relay over persistent WebSocket

## Why Legatus Exists

Legatus solves one specific problem:
**make STDIO JSON-RPC clients speak to remote transports without embedding transport logic in the client.**

## Quick Start

```bash
# Build the escript
mix escript.install hex legatus

# or
git clone git@github.com:sovetnik/legatus.git
mix escript.build

# Start your JSON-RPC HTTP server (example on port 4000)
# Then send a request:
echo '{"jsonrpc":"2.0","method":"add","params":[2,3],"id":1}' | \
  ./legatus http://localhost:4000/rpc
```

`mix escript.build` собирает `legatus` локально, а `mix escript.install` устанавливает его как команду, доступную из `PATH`.

**Expected output:**
```json
{"jsonrpc":"2.0","result":5,"id":1}
```

That's it. One line in, one line out. STDIO becomes HTTP, HTTP becomes STDIO.

## Architecture (Current)

### Layers

- `Legatus.Paramount`  
  Process orchestration and runtime lifecycle.
- `Legatus.Canalis.*`  
  Concrete channels/transports (`Stdio`, `Http`, `Ws`).
- `Legatus.Umwelt.*`  
  Interpretation pipeline:
  `Merkwelt.distinctio -> Verstand.descriptio -> Wirkwelt.portare`.
- `Legatus.Aussenwelt`  
  Boundary parse/format (`receptio` / `profanatio`).

## Usage Modes

### As Escript (recommended for production)

```bash
mix escript.build
./legatus http://localhost:4000/rpc
```

### With Bearer Token Authentication

When your upstream server requires authentication, pass the token via environment variable:

```bash
# Using escript
token=your_secret_token ./legatus http://localhost:4000/rpc

# Using escript
token=your_secret_token ./legatus http://localhost:4000/rpc
```

Legatus will automatically add the `Authorization: Bearer <token>` header to all HTTP requests.

**Editor integration example (Zed, Claude Code, etc.):**

```json
{
  "context_servers": {
    "my_server": {
      "source": "custom",
      "enabled": true,
      "command": "legatus",
      "args": ["http://localhost:4000/rpc"],
      "env": {"token": "your_secret_token"}
    }
  }
}
```

### Flows

HTTP mode:

```text
STDIN -> Aussenwelt.receptio -> Umwelt.percipere -> Canalis.Http.Client -> Aussenwelt.profanatio -> STDOUT
```

WebSocket mode:

```text
STDIN (uplink) -> Aussenwelt.receptio -> Umwelt.percipere -> Canalis.Ws.send_request
Canalis.Ws.receive_message (downlink) -> Aussenwelt.receptio -> Umwelt.percipere -> STDOUT
```

## Architecture

### Data Flow

The pipeline uses tagged tuples to track data state:

1. **Receptio** (Aussenwelt): Parse JSON  
   `{:phaenomenon, map}` | `{:fiasco, json_error}`

2. **Percipere** (Merkwelt): Validate request  
   `{:actio, map}` | `{:fiasco, error_map}`

3. **Portare** (Wirkwelt): HTTP transport  
   `{:gloria, map}` | `{:fiasco, error_map}` | `{:silentium, map}`

4. **Profanatio** (Aussenwelt): Format output  
   `{:gloria, json}` | `{:fiasco, json}` | `{:silentium, "Nullius in verba"}`

5. **Emit** (Geist): Write to STDOUT or skip

### Key Modules

- `Legatus` — entrypoint (`invoke/2`, `invoke/3`)
- `Legatus.Paramount.Http` — HTTP runtime process
- `Legatus.Paramount.Ws` — WebSocket runtime process
- `Legatus.Canalis.Stdio` — STDIN/STDOUT boundary
- `Legatus.Canalis.Http.Client` — HTTP POST transport
- `Legatus.Canalis.Ws` — stateful WS client process
- `Legatus.Paramount.Memento` — pending request ids for WS close semantics
- `Legatus.Chronica` — logging

### Runtime Semantics

- In `:http` mode, runtime terminates when STDIN reaches EOF.
- In `:websocket` mode, runtime terminates on WS close/down and emits JSON-RPC error
  `-32001` with message `"connection_closed"` for pending requests.

### Error Handling

All errors are JSON-RPC compliant:

- `-32700` Parse error (invalid JSON)
- `-32600` Invalid Request (missing method)
- `-32000` HTTP errors (4xx/5xx)
- `-32001` Transport errors (connection refused)

## Configuration

Legatus is configured via command-line arguments:

```bash
./legatus http://localhost:4000/rpc
```

Mode selection:

```bash
./legatus http://localhost:4000/rpc
./legatus ws://localhost:4000/ws --ws
```

## Testing

- Process lifecycle assertions: `ExUnitEx` (`assert_processes_started/stopped`)
- Queue/waiting semantics for WS channel memory: `test/legatus/canalis/ws/memento_test.exs`
- Boundary parse/format semantics: `Aussenwelt` tests
- Integration runtime tests:
  - `test/legatus/paramount/http_test.exs`
  - `test/legatus/paramount_test.exs`

## JSON-RPC Support

### Requests
- ✅ Standard requests with `id`
- ✅ Notifications (no `id`)
- ❌ Batches (array of requests) are not supported

### Responses
- ✅ Success responses (`result`)
- ✅ Error responses (`error`)
- ✅ HTTP 204 handling (notifications)

### Limitations

- One runtime per process invocation
- No retry/backoff policy built in
- No business logic; transport/translation only
- No JSON-RPC batch request/response support

## License

See LICENSE file.

## Etymology

- **Legatus** (Latin) — envoy, messenger
- **Aussenwelt** — outer world
- **Merkwelt** — perceptual distinction
- **Verstand** — interpretation/description
- **Wirkwelt** — action world
- **Paramount** — mount-point of runtime process reality