Skip to content

sovetnik/legatus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

# 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:

{"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)

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

With Bearer Token Authentication

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

# 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.):

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

Flows

HTTP mode:

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

WebSocket mode:

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:

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

Mode selection:

./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

About

A minimal STDIO ↔ HTTP proxy built in Elixir. Legatus bridges agents and MCP servers, translating JSON-RPC dialogue between local and remote worlds.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages