# WardrikHTTP

Serves [Wardrik](https://hexdocs.pm/wardrik) over HTTP. Every operation is a JSON route. Agents'
events stream as server-sent events, which a client resumes after a disconnect.

## Install

```elixir
def deps do
  [
    {:wardrik, "~> 0.1.0-rc.1"},
    {:wardrik_http, "~> 0.1.0-rc.1"}
  ]
end
```

## Serve it

Add `Wardrik.HTTP` to your supervision tree after Wardrik:

```elixir
defmodule MyApp.AgentRuntime do
  use Wardrik, otp_app: :my_app
end

# config/runtime.exs
config :my_app, MyApp.AgentRuntime,
  store: {WardrikStore.EKV, data_dir: "/var/lib/my_app"},
  keys: [System.fetch_env!("WARDRIK_KEY")]

# application.ex
children = [
  MyApp.AgentRuntime,
  {Wardrik.HTTP, wardrik: MyApp.AgentRuntime, port: 4000}
]
```

It also takes Bandit's options, like `:ip`. It serves plain HTTP, so put a TLS-terminating proxy
in front of it on a network.

`Wardrik.HTTP` passes `MyApp.AgentRuntime` to `WardrikHTTP` as its server. Use `WardrikHTTP` directly
only for a `Wardrik.Server` of your own.

## Make a token

Every request sends an API token as `Authorization: Bearer`. Make one in IEx:

```elixir
scope = MyApp.AgentRuntime.as("acme/@maria", roles: [:owner], resources: ["*"])
{:ok, issued} = Wardrik.Tokens.create(scope, name: "laptop", roles: ["owner"], resources: ["acme/*"])

issued.secret
#=> "wrk_3f9a61c0d4e2_Vb7..."
```

The token acts for Maria, with the roles and resources it's given. See
[Who may do what](https://hexdocs.pm/wardrik/access.html).

## Talk to agents

Define an agent, send it a message, and follow its log:

```sh
export TOKEN=wrk_3f9a61c0d4e2_Vb7...

curl -X PUT localhost:4000/v1/agents/acme/triage \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"model": "anthropic:claude-haiku-4-5", "instructions": "You watch the incident queue."}'

curl -X POST localhost:4000/v1/agents/acme/triage/messages \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"content": "What is on fire?"}'
#=> {"message": {"id": "msg_Xq3f...", "cursor": "eyJhY21lL3RyaWFnZSI6NDF9"}}

curl -N localhost:4000/v1/agents/acme/triage/events?after=eyJhY21lL3RyaWFnZSI6NDF9 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream"
#=> id: eyJhY21lL3RyaWFnZSI6NDN9
#=> data: {"specversion":"1.0","type":"wardrik.run.completed",...}
```

An agent's ID is its path: `acme/triage` for an org's agent, `acme/@maria/notes` for a person's.
Percent-encode each segment on its own.

Event streams:

* Each event's `id` is a cursor.
* A stream starts after `Last-Event-ID` or `?after=`. With neither, it starts at the next event.
* Without `Accept: text/event-stream`, the same route returns a page of events as JSON.
* An idle stream sends a comment every 15 seconds.
* A stream ends when its token is revoked.

`GET /v1/openapi.json` lists every route, for client generators. Errors are the protocol's
`{"error": {"type": ..., "message": ...}}`. Every response has a `request-id` header.

## Receive webhooks

A trigger with a `webhook` receives requests at `POST /v1/hooks/{agent}/{trigger}`. These need no
token. The request proves itself with the trigger's secret:

```elixir
:ok = MyApp.AgentRuntime.put_secret("acme", "ISSUES_HOOK", System.fetch_env!("ISSUES_HOOK"))

Wardrik.Agent.define(scope, "acme/triage",
  model: "anthropic:claude-haiku-4-5",
  triggers: [%{name: "issues", webhook: %{secret: "${ISSUES_HOOK}"}, prompt: "Triage it."}]
)
```

Point the service at `https://agents.example.com/v1/hooks/acme/triage/issues`. See
[Trigger agents](https://hexdocs.pm/wardrik/triggers.html) for how requests are verified.

## Run tools in your own process

A tool worker is your own program offering agents tools. The tools run in its process, with its
code, data and keys. The worker connects to Wardrik, so Wardrik never needs to reach it.

1. Make it a token with the `worker` role over the agents it serves:

   ```elixir
   {:ok, issued} =
     Wardrik.Tokens.create(scope, name: "shop", roles: ["worker"], resources: ["acme/agent/*"])
   ```

2. The worker opens a WebSocket to `/v1/workers` with that token and acts as an MCP server.
   Wardrik sends `initialize` and `tools/list`, then a `tools/call` for each call an agent makes:

   ```json
   {"jsonrpc": "2.0", "id": 3, "method": "tools/call",
    "params": {"name": "refund_order", "arguments": {"order": "A1"},
               "_meta": {"agent": {"key": "acme/triage", "org": "acme", "name": "triage"},
                         "run": "4bf92f3577b34da6a3ce929d0e0e4736",
                         "principal": "acme/@maria", "idempotencyKey": "call_1"}}}
   ```

3. Agents name workers' tools as `workers/*`:

   ```elixir
   Wardrik.Agent.define(scope, "acme/triage",
     model: "anthropic:claude-haiku-4-5",
     tools: ["workers/*"],
     policy: [ask: ["workers/refund_order"]]
   )
   ```

The model sees the tool as `workers__refund_order`. A tool without `readOnlyHint` counts as one
that changes things, and the agent's policy decides it like any other. A result's
`_meta.costUsd` is what the call cost, and budgets count it. See `WardrikHTTP.Worker` for the
whole exchange.

A worker's tools go when its socket closes and return when it reconnects. Each turn uses the tools
offered when it starts. Each call goes to a worker connected at that moment.

A call whose worker disconnects, or doesn't answer within a minute, may have run:

* If every worker offering the tool marks it `readOnlyHint` or `idempotentHint`, the call runs
  again on whichever worker offers it then, with the same `idempotencyKey`.
* Otherwise it waits on a person as `in_doubt`, to approve running it again or deny it.

## Sign people in

If Wardrik signs people in, `/oauth/` serves the pages a sign-in returns to. Set `page` to that
URL:

```elixir
config :my_app, MyApp.AgentRuntime,
  keys: [System.fetch_env!("WARDRIK_KEY")],
  oauth: [
    page: "https://agents.example.com/oauth",
    providers: [
      github: [
        issuer: "https://github.com/login/oauth",
        client_id: System.fetch_env!("GITHUB_CLIENT_ID"),
        client_secret: System.fetch_env!("GITHUB_CLIENT_SECRET"),
        scopes: ~w(repo),
        flow: :device
      ]
    ]
  ]
```

See [Credentials and sign-ins](https://hexdocs.pm/wardrik/credentials.html).

## In a Phoenix app

Forward a path to it from your router, in place of the child above:

```elixir
forward "/agents", Wardrik.HTTP, wardrik: MyApp.AgentRuntime
```

Routes are then under `/agents`, as `/agents/v1/agents/acme/triage`. Sign-in pages are at
`/agents/oauth`.

Webhook signatures cover the exact bytes sent. If your endpoint parses bodies first, have
`Plug.Parsers` keep them:

```elixir
plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  json_decoder: Phoenix.json_library(),
  body_reader: Wardrik.HTTP.body_reader()
```

## Your own sign-in

To authenticate requests by your app's own sessions in place of API tokens, give an
authenticator. See `WardrikHTTP.Authenticator`.

```elixir
defmodule MyAppWeb.WardrikSession do
  @behaviour WardrikHTTP.Authenticator

  @impl true
  def authenticate(conn, {_server, wardrik}, _config) do
    with ["Bearer " <> token] <- Plug.Conn.get_req_header(conn, "authorization"),
         {:ok, id} <- Phoenix.Token.verify(MyAppWeb.Endpoint, "wardrik", token, max_age: 86_400) do
      {:ok, Wardrik.as(wardrik, id, roles: [:owner], resources: [id <> "/*"])}
    else
      _not_signed_in -> {:error, Wardrik.Error.new(:unauthorized, "Sign in first.")}
    end
  end
end

forward "/agents", Wardrik.HTTP,
  wardrik: MyApp.AgentRuntime,
  authenticator: {MyAppWeb.WardrikSession, []}
```

## License

Apache-2.0
