Credentials
Copy MarkdownA tool that acts as a person needs their token. A WardrikCore.Credentials module finds it, by
the credential's name and the principal the turn acts for.
Setup
Paste this into a fresh IEx session. The model calls need an Anthropic key. MyApp.Tokens stands
in for your app's own token table:
Mix.install([{:wardrik_core, "~> 0.1.0-rc.1"}])
System.put_env("ANTHROPIC_API_KEY", "sk-ant-...")
defmodule MyApp.Tokens do
use Agent
def start_link(_opts), do: Agent.start_link(fn -> %{} end, name: __MODULE__)
def get(id, name), do: Agent.get(__MODULE__, &Map.get(&1, {id, name}))
def put(id, name, token), do: Agent.update(__MODULE__, &Map.put(&1, {id, name}, token))
def delete(id, name), do: Agent.update(__MODULE__, &Map.delete(&1, {id, name}))
end
children = [
MyApp.Tokens,
{WardrikCore.Backends.MemoryStore, name: :agents},
{WardrikCore, backend: {WardrikCore.Backends.MemoryStore, name: :agents}}
]
{:ok, _} = Supervisor.start_link(children, strategy: :rest_for_one)Replace "sk-ant-..." with your key.
A credentials module
This one finds tokens in MyApp.Tokens, and sends the person to sign in when there's none yet:
defmodule MyApp.Vault do
@behaviour WardrikCore.Credentials
@impl true
def resolve(_name, nil, _opts), do: {:error, "There's no one to act for."}
def resolve(name, %WardrikCore.Principal{id: id}, opts) do
case MyApp.Tokens.get(id, name) do
nil -> {:authorize, "#{opts[:base_url]}/connect/#{name}"}
token -> {:ok, token}
end
end
@impl true
def refused(name, %WardrikCore.Principal{id: id}, _opts), do: MyApp.Tokens.delete(id, name)
endresolve/3 returns one of:
{:ok, token}- the token.{:authorize, url}- the person must sign in first. The URL is a page in your app that starts the provider's OAuth flow for whoever is signed in.{:error, reason}- it can't help.
refused/3 is optional. A tool calls it through WardrikCore.Credentials.refused/2 when a service
rejects a token, such as with a 401. This vault forgets the token, so the next fetch sends the
person to sign in again.
Using credentials
Give the module to the agent. A tool fetches what it needs:
defmodule MyApp.Issues do
@behaviour WardrikCore.Capability
@impl true
def tools(_context, _opts) do
[
WardrikCore.Tool.new!(
name: "create_issue",
description: "Open a GitHub issue.",
schema: [title: [type: :string, required: true]],
run: fn %{title: title}, context ->
with {:ok, _token} <- WardrikCore.Credentials.fetch(context, "github") do
# Call GitHub's API with the token here.
{:ok, "Opened \"#{title}\"."}
end
end
)
]
end
end
{:ok, agent} =
WardrikCore.start_agent(
agent_id: "support",
model: "anthropic:claude-haiku-4-5",
capabilities: [MyApp.Issues],
credentials: {MyApp.Vault, base_url: "https://app.example.com"}
)
maria = %WardrikCore.Principal{id: "user:maria", attributes: %{org: "acme"}}
:ok = WardrikCore.ask(agent, "Open an issue for the login bug.", %{principal: maria})Credentials are never kept in the context, so they're never saved or emitted.
Signing in
Maria has no GitHub token yet, so the call parks on a WardrikCore.Authorization. It holds the
url, the credential and the principal, so you can show the right person where to sign in:
{:tool_calls, [{_call, {:park, %WardrikCore.Authorization{url: url}}}]} =
WardrikCore.parked(agent)
url
#=> "https://app.example.com/connect/github"- Remember which agent asked, such as in the OAuth
stateyour app signs. - In the callback, store the token in
MyApp.Tokens. - Resume that agent. Its id finds it on any node in the cluster. The call runs again, this time with the token:
:ok = MyApp.Tokens.put("user:maria", "github", "gho_...")
"support"
|> WardrikCore.whereis()
|> WardrikCore.resume(%{}, %{by: maria})Another agent parked on the same sign-in still shows its own link. When the person opens it, your app finds the token already stored and resumes that agent straight away. They only sign in once.
To watch who's waiting on what, such as for a dashboard or an alert, handle
[:wardrik_core, :turn, :stop]. It carries a parked turn's reason. Handlers run in the agent's
own process, so use them to watch, not to resume agents.
Keys
Credentials are what a tool needs to act as a person. Keys are what your code needs as itself: a
model provider's key, or a search API's. Give the agent a WardrikCore.Keys module. This one
reads the environment:
defmodule MyApp.Keys do
@behaviour WardrikCore.Keys
@impl true
def fetch(name, _principal, _opts), do: System.fetch_env(name)
end
:ok = WardrikCore.configure(agent, keys: MyApp.Keys)Keys are fetched by name, for the agent's principal, so an org can bring its own.
- Every model call fetches its provider's key by the name ReqLLM gives it, such as
ANTHROPIC_API_KEY, and passes it to that call only. - A capability or middleware fetches with
WardrikCore.Keys.fetch(context, "BRAVE_API_KEY"). - If no key is held, ReqLLM finds one in its config or the environment.
Keep keys apart from credentials. A credential's name comes from what an agent's owner configures. A key's name never should.