WardrikHTTP
Copy MarkdownServes Wardrik over HTTP. Every operation is a JSON route. Agents' events stream as server-sent events, which a client resumes after a disconnect.
Install
def deps do
[
{:wardrik, "~> 0.1.0-rc.1"},
{:wardrik_http, "~> 0.1.0-rc.1"}
]
endServe it
Add Wardrik.HTTP to your supervision tree after Wardrik:
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:
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.
Talk to agents
Define an agent, send it a message, and follow its log:
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
idis a cursor. - A stream starts after
Last-Event-IDor?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:
: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 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.
Make it a token with the
workerrole over the agents it serves:{:ok, issued} = Wardrik.Tokens.create(scope, name: "shop", roles: ["worker"], resources: ["acme/agent/*"])The worker opens a WebSocket to
/v1/workerswith that token and acts as an MCP server. Wardrik sendsinitializeandtools/list, then atools/callfor each call an agent makes:{"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"}}}Agents name workers' tools as
workers/*: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
readOnlyHintoridempotentHint, the call runs again on whichever worker offers it then, with the sameidempotencyKey. - 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:
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
]
]
]In a Phoenix app
Forward a path to it from your router, in place of the child above:
forward "/agents", Wardrik.HTTP, wardrik: MyApp.AgentRuntimeRoutes 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:
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.
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