# WardrikDaemon

Wardrik as a daemon: agents from files, `config.yaml`, an HTTP API, and the `wardrik` command.

## Try it

```sh
curl -fsSL https://github.com/wardrik-dev/wardrik_daemon/releases/download/v0.1.0-rc.1/install.sh | WARDRIK_VERSION=0.1.0-rc.1 sh

wardrik init --org acme
```

`init` asks what it needs and says what it does:

1. the model provider, those whose key is set here first, and the model: `claude-haiku-4-5` for
   Anthropic.
2. the provider's key, not shown as you type, unless the environment or the env file sets it.
3. three files, keeping any already there:
   * `~/.config/wardrik/config.yaml` - the deployment's settings, with the default model.
   * `~/.config/wardrik/agents/assistant.md` - the front door.
   * `~/.config/wardrik/env` - keys the daemon reads. Only you can read it.
4. the daemon, as a systemd user service where there's one, or else with `wardrik start`.
5. the key, sealed in the vault, unless the env file sets it.
6. the front door, asked one short question.

Without a terminal, flags answer:

```sh
wardrik init --provider anthropic --model claude-sonnet-5 --key-from-env MY_KEY --no-service --no-ask
```

`--yes` runs the daemon as a systemd service. To keep the key yourself, put it in the env file
before `init`, and it's read from there:

```sh
echo 'ANTHROPIC_API_KEY=sk-ant-...' >> ~/.config/wardrik/env
```

The front door gets `web_search` where `BRAVE_API_KEY` is set as you run `init`. Elsewhere
`config.yaml` says how to turn it on.

The daemon runs in the background. Every other command reaches it over its HTTP API, at
`127.0.0.1:4747` unless `config.yaml` says otherwise.

A key sealed in the vault is read as each call is made, so a new key needs no restart. Add `--org`
to seal an org's own key.

```sh
echo "$KEY" | wardrik vault put ANTHROPIC_API_KEY
```

Tell the front door an outcome, as "tell me when https://elixir-lang.org/atom.xml has a new post".
It proposes the work as the calls that do it: a worker it defines, and the triggers that start it.
Approve them with `wardrik approval approve`, and the work runs on its own. The front door's body
is its instructions, then a playbook to edit as you like.

The installer checks the release against its `SHA256SUMS`, puts it in `~/.local/lib/wardrik`
beside the one before it, and `wardrik` in `~/.local/bin`. Without `WARDRIK_VERSION` it installs
the newest release, pre-releases included. The release runs on Linux, x86_64 or aarch64, with
glibc 2.35 or later. Elsewhere, run the image:

```sh
docker run -d --name wardrik -p 127.0.0.1:4747:4747 \
  -v wardrik-config:/config -v wardrik:/data ghcr.io/wardrik-dev/wardrik:0.1.0-rc.1
docker exec -it wardrik wardrik init
```

`docker build .` builds the image, and `docker build --target tarball --output dist .` the
tarball.

In a checkout of this repository, `mix release` builds the same release. Without one,
`mix wardrik serve`, `mix wardrik ls` and the rest run the same commands.

## Agents

An agent is a Markdown file in `agents/`. The frontmatter is its definition, and the body is its
instructions:

```markdown
---
description: Watches the incident queue and says what needs an engineer.
model: anthropic:claude-sonnet-5
tools: [linear/*]
policy: careful
triggers:
  - {name: daily, every: "0 9 * * *", timezone: America/Los_Angeles, prompt: What's new?}
channels: [slack]
---
You watch the incident queue. Summarise anything new, and say which issues need an engineer.
```

Its ID is the deployment's org and the file's name, as `acme/triage`. With `user: jon`, it's
`acme/@jon/triage`. Keys the file leaves out come from `config.yaml`'s `defaults`.

`wardrik reload` reads the files again:

* a changed file's agent uses its new definition from its next turn.
* a removed file's agent is forgotten.
* a file that doesn't read is logged, and `wardrik doctor` says why.

The API can't forget an agent a file declares.

The daemon offers three tools of its own:

* `run_lua` - Lua in a sealed sandbox, for arithmetic, reshaping data and the current time. See
  [WardrikLua](https://hexdocs.pm/wardrik_lua).
* `web_search` - searches through Brave. Needs `brave.api_key`. See
  [WardrikBrave](https://hexdocs.pm/wardrik_brave).
* `deep_research` - splits a topic into angles, and has a subagent with the agent's own tools
  research each at once. See [WardrikResearch](https://hexdocs.pm/wardrik_research).

With `files` in its `tools`, an agent keeps its own tree of files in the store. `context:
[MEMORY.md]` reads files into every turn: that's its memory. See
[WardrikWorkspace](https://hexdocs.pm/wardrik_workspace).

Policies are YAML files in `policies/`, by name: `policies/careful.yaml` is `policy: careful`.

## config.yaml

```yaml
org: acme
base_url: https://agents.acme.example   # where people and services reach it

defaults:
  model: anthropic:claude-haiku-4-5

mcp:
  github:
    url: https://api.githubcopilot.com/mcp/   # its full endpoint, used as given
    credential: github
    only: [list_commits, get_commit]

oauth:
  providers:
    github:
      issuer: https://github.com/login/oauth
      resource: https://api.githubcopilot.com/mcp/
      client_id: ${GITHUB_CLIENT_ID}
      client_secret: ${GITHUB_CLIENT_SECRET}
      scopes: [repo]
      flow: device

channels:
  slack:
    type: slack
    token: ${SLACK_BOT_TOKEN}
    app_token: ${SLACK_APP_TOKEN}       # Socket Mode; or signing_secret, over HTTP
    approvers: ["@maria"]

brave:
  api_key: ${BRAVE_API_KEY}             # web_search; or seal it: wardrik vault put BRAVE_API_KEY

notify:
  ops: {channel: slack, room: "#ops", approvers: ["@maria"]}
  audit: {webhook: {url: https://hooks.example.com/wardrik, secret: "${WEBHOOK_SECRET}"}}

http:
  port: 4747
  ip: 127.0.0.1

storage:
  postgres:
    url: ${DATABASE_URL}                # postgres://user:pass@host/db; ?ssl=true for TLS
```

`defaults` is what every agent gets where its definition says nothing. That holds however it was
defined: from a file, over the HTTP API, or by the front door.

`${NAME}` is replaced with the environment variable `NAME`, one the `env` file sets, or the
deployment's secret `NAME`, which `wardrik vault put NAME` seals. `vault`, `http` and `storage`
are read before the vault opens, so they take the environment alone.

An unknown key is an error, and the daemon says where. `tools`, `tokens`, `cluster` and `ssh`
aren't supported yet, and are skipped with a warning.

The vault's key is `vault.keys`, or `WARDRIK_VAULT_KEY`, or `vault_key` in the data home, made the
first time the daemon starts.

### models

Prices for models the catalog has no dollar price for, so budgets and spending count them. Give
`input` and `output`, and optionally `cache_read` and `cache_write`, in dollars per million
tokens. Or price it as another model with `priced_as`. Either takes a `markup`, as `0.1` for 10%
more.

```yaml
models:
  zai_coding_plan:glm-5.1: {priced_as: zai:glm-5.1}
  openrouter:qwen3-coder: {input: 0.3, output: 1.2}
```

### workspace

`workspace.shared` names the file trees agents beyond their owners may mount. Each lists, by
resource pattern, the agents that may `read` it and those that may `write` it:

```yaml
workspace:
  shared:
    acme/runbooks:
      read: ["acme/*"]
      write: [acme/agent/ops, "acme/agent/ops/*"]
```

### notify

`notify` names the targets agents' notices go to: a room of a channel, or a webhook. An agent file
routes notices to a target, or straight to a room:

```yaml
notify: {to: ["slack:#updates"], on: [finished, failed]}
```

### storage

`storage` keeps everything in Postgres. Point it at the primary. The daemon creates its table as
it starts, one node at a time. Without `storage`, everything is kept on the node's disk.

### orgs

`orgs` declares the orgs beside the deployment's own `org`. Each lists the servers, channels and
notify targets its agents use, by their names in those sections, and the policy in `policies/` it's
held to:

```yaml
org: platform

orgs:
  "*":                       # every org
    mcp: [search]
    policy: base
  payments:
    mcp: [stripe, github]
    channels: [payments-slack]
    notify: [audit]
    policy: payments-strict
  growth:
    mcp: [github]
```

* Every agent of an org is held to `"*"`'s policy, its org's policy, and its own definition's. The
  strictest decides. A definition can narrow them, never widen them.
* A policy's budget is the org's: its agents spend from it together. Each org spends `"*"`'s
  budget apart from the others. A budget per turn stays each agent's own.
* An org whose policy isn't in `policies/` runs none of its agents.
* A server, channel or target no entry lists is the deployment's own org's.
* A channel serves one org, or every org if `"*"` lists it.
* Any other org is refused: no agent, secret, server or token is kept for it.

Each org's agents reach a server with their own credential. Here, payments and growth each seal
their own `github`:

```sh
wardrik vault put github --org payments
```

## The wardrik command

`wardrik help` lists every command, and `wardrik help COMMAND` says more about one. Every command
takes `--json`.

| Command | Does |
| --- | --- |
| `init`, `doctor`, `version`, `completion SHELL` | sets up the front door and starts the daemon, checks what the daemon reads, prints the version, writes shell completion |
| `serve`, `start`, `stop`, `pid`, `logs` | runs the daemon in the foreground or background, stops it, prints its process id, follows its log |
| `service install`, `service uninstall` | runs it as a systemd user service, or stops doing so |
| `status`, `reload`, `audit` | says where it runs, reads the agent files again, shows what people changed |
| `agent ls` (`ls`), `agent show AGENT` | lists agents, shows one |
| `agent ask AGENT MESSAGE` (`ask`), `agent follow AGENT` (`follow`) | asks and prints the answer as it comes, follows what an agent does |
| `agent history`, `agent cancel`, `agent rm`, `agent calls`, `agent kill`, `agent move` | an agent's conversation, stopping its turn, removing it, its running tool calls, stopping one, moving it to another node |
| `approval ls`, `approval approve AGENT CALL`, `approval deny AGENT CALL` | the calls agents wait on a person for |
| `trigger ls`, `trigger set`, `trigger rm` | an agent's triggers |
| `server ls`, `server add`, `server rm` | MCP servers an owner adds |
| `connection ls`, `connection connect`, `connection disconnect` | accounts an owner connects |
| `token create`, `token ls`, `token revoke` | API tokens |
| `vault put`, `vault ls`, `vault rm` | secrets: the deployment's, or an org's or a person's with `--org` or `--user` |

The command acts with a token the daemon writes to `cli_token` in the data home each time it starts.
`WARDRIK_URL` and `WARDRIK_TOKEN` point it at another daemon.

## Directories

| Variable | Default | Holds |
| --- | --- | --- |
| `WARDRIK_CONFIG_DIR` | `~/.config/wardrik` | `config.yaml`, `agents/`, `policies/` and `env` |
| `WARDRIK_DATA_DIR` | `~/.local/share/wardrik/store` | the store |
| `WARDRIK_DATA_HOME` | `~/.local/share/wardrik` | `vault_key`, `cli_token` and the node's `cookie` |
| `WARDRIK_STATE_DIR` | `~/.local/state/wardrik` | logs |
| `WARDRIK_ENV_FILE` | `env` in the config directory | variables the daemon sets where its environment doesn't |
