Wardrik behaviour (wardrik v0.1.0-rc.1)

Copy Markdown

Agents, governed: the agents your app runs, the policies they're held to, and the operations people and programs reach them through.

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

children = [MyApp.AgentRuntime]

scope = MyApp.AgentRuntime.as("acme/@maria", roles: [:owner], resources: ["acme/@maria/*"])
Wardrik.Agent.stream(scope, "acme/@maria/assistant", "What's on today?") |> Enum.each(&IO.write/1)

Options

Each place overrides the one before it:

# 1. config/runtime.exs
config :my_app, MyApp.AgentRuntime, store: {WardrikStore.EKV, data_dir: "/var/lib/my_app"}

# 2. The supervision tree
children = [{MyApp.AgentRuntime, deployment: "production"}]

# 3. init/1, for what's only known at runtime
@impl Wardrik
def init(config), do: {:ok, Keyword.put(config, :keys, [System.fetch_env!("WARDRIK_KEY")])}

See start_link/1 for every option.

Summary

Types

Who acts through Wardrik, or whom an agent works for. Build one with principal/2.

t()

Callbacks

Fills in options only known at runtime, before the module starts.

Functions

Returns the scope principal acts in on instance.

Returns the scope of the API token secret on instance.

Checks a definition's policy.

Returns a specification to start this module under a supervisor.

Returns Wardrik's OAuth Client ID Metadata Document.

Returns a cursor for events.list to read on from.

Removes owner's secret name, and records it in the audit. Returns :ok if there's none.

Returns owner's secret name, unsealed.

Finishes a browser sign-in from its callback params. Returns the provider signed in to.

Adds a statement to scope. opts are as Wardrik.Statement.new/1 takes them.

Returns the name of the API key model's provider needs, or nil.

Lists every secret as {owner, name, kind}, or with owner, the names of theirs.

Returns owner's secret name, unsealed, with its kind and version.

Returns the positions in cursor, or :error if it isn't one. The reverse of cursor/1.

Returns the principal with id, such as "acme/@maria", and optional attributes.

Seals value as owner's secret name, and records it in the audit.

Applies the org's current settings to its awake agents.

Runs the operation id, such as "agent.ask", in scope.

Returns the plug for sign-in callback pages, as {plug, opts}, or nil without :oauth.

Starts an agent on instance.

Starts Wardrik.

Streams the operation id, such as "events.list", in scope.

Types

principal()

@type principal() :: WardrikCore.Principal.t()

Who acts through Wardrik, or whom an agent works for. Build one with principal/2.

t()

@type t() :: %Wardrik{
  authorizer: {module(), term()},
  backend: {module(), keyword()},
  capabilities: %{required(String.t()) => WardrikCore.Context.entry()},
  channels: %{required(String.t()) => {module(), keyword()}},
  declarations: {module(), term()} | nil,
  defaults: map(),
  deployment: String.t(),
  egress: (String.t() -> :ok | {:error, String.t()}),
  idle_timeout: pos_integer() | :infinity,
  keys: [String.t()],
  listeners: [module() | {module(), keyword()}],
  mcp: keyword() | nil,
  middleware: [WardrikCore.Context.entry()],
  name: atom(),
  notify: %{
    targets: %{required(String.t()) => {module(), keyword()}},
    retries: keyword()
  },
  oauth: keyword() | nil,
  orgs: {module(), term()} | nil,
  policies: %{required(String.t()) => WardrikPolicy.t()},
  policy: WardrikPolicy.t() | nil,
  prices: %{required(String.t()) => WardrikCore.Usage.price()},
  retention: pos_integer() | :infinity,
  store: WardrikStore.t(),
  triggers: keyword(),
  trusted_workers: boolean(),
  workspace: %{
    required(String.t()) => %{read: [String.t()], write: [String.t()]}
  }
}

Callbacks

init(config)

@callback init(config :: keyword()) :: {:ok, keyword()}

Fills in options only known at runtime, before the module starts.

@impl Wardrik
def init(config), do: {:ok, Keyword.put(config, :keys, [System.fetch_env!("WARDRIK_KEY")])}

Functions

as(instance, principal, grants \\ [])

@spec as(atom() | t(), WardrikCore.Principal.t() | String.t(), term()) ::
  Wardrik.Scope.t()

Returns the scope principal acts in on instance.

grants is a list of Wardrik.Statements, or the options for one. Nothing is allowed unless it's granted.

scope = Wardrik.as(MyApp.AgentRuntime, "acme/@maria", roles: [:reader], resources: ["acme/agent/*"])

authenticate(instance, secret)

@spec authenticate(atom() | t(), String.t()) ::
  {:ok, Wardrik.Scope.t()} | {:error, Wardrik.Error.t()}

Returns the scope of the API token secret on instance.

Returns an unauthorized error if the token is unknown or has expired.

{:ok, scope} = Wardrik.authenticate(MyApp.AgentRuntime, secret)

check_policy(policy)

@spec check_policy(map()) :: :ok | {:error, String.t()}

Checks a definition's policy.

Returns :ok, or the error defining the agent would give.

Wardrik.check_policy(%{"judge" => %{"questions" => [%{"question" => "Publish?"}]}})
#=> {:error, "a question decides nothing; give it deny, ask or allow"}

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

client_document(instance)

@spec client_document(atom() | t()) :: map()

Returns Wardrik's OAuth Client ID Metadata Document.

The sign-in pages serve it at /client. Use it if your app serves the document itself.

cursor(positions)

@spec cursor(%{required(String.t()) => non_neg_integer()}) :: String.t()

Returns a cursor for events.list to read on from.

positions maps each agent or conversation ID to the last sequence read from its log.

Wardrik.cursor(%{"acme/triage" => 41})

delete_secret(instance, owner, name)

@spec delete_secret(atom() | t(), String.t(), String.t()) :: :ok | {:error, term()}

Removes owner's secret name, and records it in the audit. Returns :ok if there's none.

fetch_secret(instance, owner, name)

@spec fetch_secret(atom() | t(), String.t(), String.t()) ::
  {:ok, binary()} | {:error, term()}

Returns owner's secret name, unsealed.

finish_sign_in(instance, params)

@spec finish_sign_in(atom() | t(), map()) :: {:ok, String.t()} | {:error, term()}

Finishes a browser sign-in from its callback params. Returns the provider signed in to.

Use it if your app serves the callback page itself. params holds "state", and "code" or "error".

grant(scope, opts)

@spec grant(Wardrik.Scope.t(), keyword()) :: Wardrik.Scope.t()

Adds a statement to scope. opts are as Wardrik.Statement.new/1 takes them.

MyApp.AgentRuntime.as("acme/@maria") |> Wardrik.grant(roles: [:approver], resources: ["acme/agent/*"])

key_name(model)

@spec key_name(String.t()) :: String.t() | nil

Returns the name of the API key model's provider needs, or nil.

Calls to model use the secret of that name, the owner's or the deployment's.

Wardrik.key_name("zai_coding_plan:glm-5.1")
#=> "ZAI_API_KEY"

list_secrets(instance)

@spec list_secrets(atom() | t()) :: [{String.t(), String.t(), WardrikVault.kind()}]

Lists every secret as {owner, name, kind}, or with owner, the names of theirs.

Values aren't unsealed. Returns [] if Wardrik was started without :keys.

list_secrets(instance, owner)

@spec list_secrets(atom() | t(), String.t()) :: [String.t()]

lookup_secret(instance, owner, name)

@spec lookup_secret(atom() | t(), String.t(), String.t()) ::
  {:ok, WardrikVault.kind(), binary(), term()}
  | {:error, term()}
  | {:error, WardrikVault.kind(), term()}

Returns owner's secret name, unsealed, with its kind and version.

The kind is :secret, or :grant for an account someone connected. Pass the version to put_secret/5 as :if_vsn.

positions(cursor)

@spec positions(String.t()) ::
  {:ok, %{required(String.t()) => non_neg_integer()}} | :error

Returns the positions in cursor, or :error if it isn't one. The reverse of cursor/1.

principal(id, attributes \\ %{})

@spec principal(String.t(), map()) :: principal()

Returns the principal with id, such as "acme/@maria", and optional attributes.

Wardrik.principal("acme/@maria", %{name: "Maria"})

put_secret(instance, owner, name, value, opts \\ [])

@spec put_secret(atom() | t(), String.t(), String.t(), binary(), keyword()) ::
  :ok | {:error, term()}

Seals value as owner's secret name, and records it in the audit.

owner is an org, as "acme", or a person, as "acme/@maria". Their agents' tools and servers can use the secret. Returns {:error, :no_keys} if Wardrik was started without :keys.

:ok = Wardrik.put_secret(MyApp.AgentRuntime, "acme", "crm-key", System.fetch_env!("CRM_KEY"))

Options

  • :if_vsn - only write if the secret is still at this version. See lookup_secret/3.

refresh(instance, opts)

@spec refresh(atom() | t(), keyword()) :: [String.t()]

Applies the org's current settings to its awake agents.

Sleeping agents pick them up when they wake. Returns a line for each agent it couldn't update.

Wardrik.refresh(MyApp.AgentRuntime, org: "acme")
#=> []

run(scope, id, input)

@spec run(Wardrik.Scope.t(), String.t(), map()) ::
  {:ok, term()} | {:error, Wardrik.Error.t()}

Runs the operation id, such as "agent.ask", in scope.

input follows the operation's input schema.

Wardrik.run(scope, "agent.ask", %{"agent" => "acme/triage", "content" => "What's on fire?"})
#=> {:ok, %{"message" => %{"id" => "msg_Xq3f...", "cursor" => "eyJhY21lL3RyaWFnZSI6NDF9"}}}

sign_in_pages(instance)

@spec sign_in_pages(atom() | t()) :: {module(), term()} | nil

Returns the plug for sign-in callback pages, as {plug, opts}, or nil without :oauth.

Mount it at the :oauth option's page.

Wardrik.sign_in_pages(MyApp.AgentRuntime)
#=> {WardrikOAuth.Plug, MyApp.AgentRuntime.OAuth}

start_agent(instance, opts)

@spec start_agent(atom() | t(), keyword()) :: {:ok, pid()} | {:error, term()}

Starts an agent on instance.

opts are as WardrikCore.start_agent/2 takes them. :policy is the agent's own WardrikPolicy, or its rules. The agent is held to its own policy, its org's and Wardrik's. The strictest decides.

Wardrik.start_agent(MyApp.AgentRuntime,
  agent_id: "acme/books",
  model: "anthropic:claude-haiku-4-5",
  policy: [ask: ["books/pay_bill"]]
)

start_link(wardrik)

@spec start_link(keyword() | t()) :: Supervisor.on_start()

Starts Wardrik.

Wardrik.start_link(
  name: MyApp.AgentRuntime,
  store: {WardrikStore.EKV, data_dir: "/var/lib/my_app"},
  keys: [System.fetch_env!("WARDRIK_KEY")],
  policy: [deny: ["shell/*"]]
)

Store

Where agents and their events are kept. In memory unless set, and gone when it stops.

# On disk
store: {WardrikStore.EKV, data_dir: "/var/lib/my_app"}

# In your app's Postgres; create the table with WardrikStore.Ecto.Migration
store: {WardrikStore.Ecto, repo: MyApp.Repo}

# In a Postgres of its own, which migrates itself
store: {WardrikStore.Ecto, url: "postgres://user:pass@db.example.com/my_app"}

# A store you've built; one already running is left as it is
store: WardrikStore.EKV.new(name: MyApp.Store, data_dir: "/var/lib/my_app")

WardrikStore.Ecto needs :ecto_sql and :postgrex in your deps.

Options

  • :name - required. What Wardrik is reached by.

  • :store - see above.

  • :deployment - the deployment's name in each event's source. "local" unless set.

  • :policy - the WardrikPolicy every agent is held to, or its rules, as [deny: ["shell/*"]].

  • :keys - keys to seal credentials with. Without them, no secrets are kept.

  • :oauth - services people sign in to, as [page: ..., providers: ..., client_name: ...]. Needs :keys. See WardrikOAuth.

  • :mcp - MCP servers agents may call, as [github: [transport: ..., orgs: ["acme"]]]. See WardrikMCP.

  • :capabilities - tools definitions name, as [files: {MyApp.Files, []}].

  • :policies - policies definitions name, as [careful: [ask: ["github/*"]]].

  • :defaults - what every definition gets where it says nothing, as %{"model" => "anthropic:claude-haiku-4-5"}.

  • :middleware - wraps every agent's calls.

  • :idle_timeout - seconds a defined agent idles before it sleeps, or :infinity. 900.

  • :channels - chat apps, as [slack: {WardrikSlack, opts}]. See Wardrik.Channel.

  • :notify - where notices go. See Wardrik.Notify.

  • :declarations - agents defined outside the API. See Wardrik.Declarations.

  • :orgs - per-org settings. See Wardrik.Orgs.

  • :workspace - file trees shared beyond their owners. See Wardrik.Workspaces.

  • :triggers - [req_options: ..., reconcile: ...]. See WardrikTriggers.

  • :listeners - listeners to start. See Wardrik.Listener.

  • :authorizer - {module, config}. {Wardrik.Statements, []} unless set.

  • :retention - days events are kept, or :infinity. 30.

  • :backend - where agents are kept. The store unless set.

  • :discovery_interval_ms - how often a node looks for agents no node runs. 30_000.

  • :trusted_workers - true to treat tool workers' answers as the deployment's own. false.

  • :egress - which URLs users may point it at. &Wardrik.Egress.public/1 unless set.

  • :prices - model prices the catalog lacks, per million tokens. See Wardrik.Setup.price/2.

    prices: %{"zai_coding_plan:glm-5.1" => [priced_as: "zai:glm-5.1", markup: 0.1]}

stream(scope, id, input)

@spec stream(Wardrik.Scope.t(), String.t(), map()) ::
  {:ok, Enumerable.t()} | {:error, Wardrik.Error.t()}

Streams the operation id, such as "events.list", in scope.

The stream yields {event, cursor}. It reads what's there, then follows new events.

{:ok, stream} = Wardrik.stream(scope, "events.list", %{"resources" => ["acme/*"]})