Credentials

Copy Markdown

A 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)
end

resolve/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"
  1. Remember which agent asked, such as in the OAuth state your app signs.
  2. In the callback, store the token in MyApp.Tokens.
  3. 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.