Wardrik behaviour (wardrik v0.1.0-rc.1)
Copy MarkdownAgents, 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.
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
@type principal() :: WardrikCore.Principal.t()
Who acts through Wardrik, or whom an agent works for. Build one with principal/2.
@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
Functions
@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/*"])
@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)
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"}
Returns a specification to start this module under a supervisor.
See Supervisor.
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.
@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})
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.
Use it if your app serves the callback page itself. params holds "state", and "code" or
"error".
@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/*"])
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"
@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.
@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.
@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.
Returns the principal with id, such as "acme/@maria", and optional attributes.
Wardrik.principal("acme/@maria", %{name: "Maria"})
@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. Seelookup_secret/3.
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")
#=> []
@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"}}}
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}
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"]]
)
@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'ssource."local"unless set.:policy- theWardrikPolicyevery 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. SeeWardrikOAuth.:mcp- MCP servers agents may call, as[github: [transport: ..., orgs: ["acme"]]]. SeeWardrikMCP.: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}]. SeeWardrik.Channel.:notify- where notices go. SeeWardrik.Notify.:declarations- agents defined outside the API. SeeWardrik.Declarations.:orgs- per-org settings. SeeWardrik.Orgs.:workspace- file trees shared beyond their owners. SeeWardrik.Workspaces.:triggers-[req_options: ..., reconcile: ...]. SeeWardrikTriggers.:listeners- listeners to start. SeeWardrik.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-trueto treat tool workers' answers as the deployment's own.false.:egress- which URLs users may point it at.&Wardrik.Egress.public/1unless set.:prices- model prices the catalog lacks, per million tokens. SeeWardrik.Setup.price/2.prices: %{"zai_coding_plan:glm-5.1" => [priced_as: "zai:glm-5.1", markup: 0.1]}
@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/*"]})