Status: Implemented
This page covers the three configuration sections that together control who can talk to Calciforge and which AI backend handles their messages:
[[agents]] — the AI backends Calciforge can call[[identities]] — the people or service accounts Calciforge recognizes,
plus their per-channel aliases[[routing]] — the rules that map identities to agentsThe short version: an incoming chat message becomes an identity, the identity chooses a route, and the route chooses an agent. It is a little like Howl's door dial, but with fewer colors and more audit logs.
Channel message arrives
│
▼
Identity lookup [[identities]] — alias (channel + id) → identity
│
▼
Routing rule [[routing]] — identity → default_agent + allowed_agents
│
▼
Agent dispatch [[agents]] — build adapter, send message, return reply
│
▼
Reply sent back to user
[[agents]])Each [[agents]] entry defines one AI backend. The kind field selects the
adapter, which is the small piece of Calciforge that knows how to talk to that
backend. All other fields are adapter-specific.
First-class adapters carry a stronger maintenance promise than generic wrappers: Calciforge should document how user messages arrive, how callbacks are authenticated, and how model/tool/web traffic is protected for that adapter. Regressions in those paths are Calciforge bugs when the upstream runtime gives us enough control to fix them. Generic command-line, generic ACP (Agent Client Protocol), and recipe adapters remain best-effort unless their recipe documents a tested boundary. In hardened profiles, prefer first-class adapters or explicitly verified recipes.
| Field | Required | Default | Description |
|---|---|---|---|
id |
yes | — | Unique name used in routing and !switch commands |
kind |
yes | — | Adapter type (see below) |
timeout_ms |
no | adapter default | Per-request timeout in milliseconds |
model |
no | — | Model name forwarded to the backend |
api_key |
no | — | Bearer token for the backend; overrides CALCIFORGE_AGENT_TOKEN |
api_key_file |
no | — | Path to file containing the API key (preferred over inline api_key) |
auth_token |
no | — | Legacy alias for api_key (openclaw-channel) |
aliases |
no | [] |
Additional names matched by !switch |
allow_model_override |
no | false |
Whether !model overrides from identities are forwarded |
registry |
no | — | Optional metadata shown in !agents output (see below) |
kind = "openclaw-channel"HTTP adapter for an OpenClaw gateway that has the Calciforge bridge plugin
installed. HTTP is the web protocol used for the request between the two
services. The plugin package is still named calciforge-channel for
compatibility, but Calciforge owns the user-facing channel. Calciforge POSTs
each routed message to the plugin's
/calciforge/inbound route, OpenClaw runs the selected agent lane with its own
session state, and the plugin sends the reply back to Calciforge's
/hooks/reply callback.
This is not a Calciforge-to-Calciforge adapter. Do not point
openclaw-channel at another Calciforge gateway. Use openai-compat for a
plain model gateway, or route to the actual downstream OpenClaw gateway that
owns the bridge plugin.
Calciforge controls identity routing, channel access, callback authentication, and artifact delivery for this path. OpenClaw's outbound model/tool traffic is only covered by Calciforge's security layers when you configure the OpenClaw service to use a tested proxy/tool/policy integration; installing the channel plugin alone does not prove outbound egress enforcement. In managed inspecting-proxy mode, prompt-injection response blocking is the default safety gate. Outbound exfiltration heuristics and high-entropy response secret-leak detection are operator opt-ins because they can be noisy on provider/tool transcripts.
Required at runtime: endpoint, plus api_key or api_key_file unless the
deployment intentionally relies on CALCIFORGE_AGENT_TOKEN. Use
reply_auth_token_file for callback auth on managed installs; inline
reply_auth_token remains supported for small local tests.
For installer-managed OpenClaw hosts, calciforge install also requires the
matching auth_token/auth_token_file, reply_webhook, and
reply_auth_token/reply_auth_token_file in the --claw spec. The installer
writes those into the remote
calciforge-channel plugin entry, installs the plugin files under
~/.openclaw/extensions/calciforge-channel, adds the plugin to
plugins.allow when an allowlist is present, and restarts the OpenClaw gateway
service.
By default, the plugin runs OpenClaw with deliver: false, reads the assistant
reply from the Calciforge session, and posts that reply back to Calciforge's
reply webhook. This keeps Calciforge-originated requests isolated from unrelated
OpenClaw channel delivery configured on the same node. The native OpenClaw
channel runtime remains available only when the plugin config sets
useNativeChannelRuntime: true.
The plugin status endpoint also reports the OpenClaw default agent runtime and
the configured primary/fallback model route. calciforge doctor fails the
agent check when the selected runtime cannot load one of those providers, so a
broken fallback cannot pass deployment preflight and later surface as a generic
OpenClaw run error.
[[agents]]
id = "primary-agent"
kind = "openclaw-channel"
endpoint = "http://127.0.0.1:18789"
api_key_file = "~/.config/calciforge/secrets/primary-agent-token"
reply_auth_token_file = "~/.config/calciforge/secrets/primary-agent-reply-token"
timeout_ms = 120000
aliases = ["main"]
registry = { display_name = "Primary Agent", specialties = ["general", "homelab-ops"] }
openclaw_agent_id (optional) sets the lane id sent to the gateway; defaults
to this agent's id.
!new <name> and !switch <agent> <name> select a Calciforge-managed
OpenClaw session lane. Calciforge keeps the old default lane when no session is
selected, and adds the selected name to the OpenClaw sessionKey when one is
active.
reply_port (optional, default 18797) is the local port Calciforge listens on
for async /hooks/reply callbacks when the gateway pushes replies
asynchronously instead of returning them synchronously.
reply_auth_token_file or reply_auth_token (optional) — bearer token required
on incoming /hooks/reply callbacks.
Installer example:
calciforge install \
--calciforge-host calciforge@calciforge.lan \
--claw 'name=primary-agent,adapter=openclaw-channel,host=root@openclaw.lan,endpoint=http://openclaw.lan:18789,auth_token=REPLACE_WITH_INBOUND_TOKEN,reply_webhook=http://calciforge.lan:18797/hooks/reply,reply_auth_token=REPLACE_WITH_REPLY_TOKEN'
Use the same inbound token in the Calciforge agent api_key/api_key_file,
and the same reply token in reply_auth_token_file/reply_auth_token.
kind = "openai-compat"Generic OpenAI-compatible HTTP endpoint (Ollama, LM Studio, Anthropic,
Together, or any endpoint that accepts /v1/chat/completions).
Required: endpoint. Recommended: model.
[[agents]]
id = "local-llm"
kind = "openai-compat"
endpoint = "http://127.0.0.1:11434"
model = "llama3.2"
timeout_ms = 180000
allow_model_override = true
Without model, Calciforge will not forward a model name to the backend
unless allow_model_override = true and the identity sets !model.
allow_model_override is explicit because Calciforge model selectors are not
portable across every agent API. Enable it only for agents that are wired to
Calciforge's model gateway or are known to accept the selected model names.
Current adapters with an implemented model-override path are openai-compat,
hermes, zeroclaw-http, zeroclaw-native, codex-cli, claude-cli,
kimi-cli, cli, and artifact-cli; all still require
allow_model_override = true before the chat command forwards a user's selected
model.
kind = "zeroclaw"Direct ZeroClaw agent endpoint (legacy; use openclaw-channel for new
deployments).
Required: endpoint, api_key.
[[agents]]
id = "zeroclaw"
kind = "zeroclaw"
endpoint = "http://127.0.0.1:18792"
api_key_file = "~/.config/calciforge/secrets/zeroclaw-token"
timeout_ms = 90000
kind = "hermes"HTTP adapter for a running Hermes API server. Hermes keeps its own session
state through the X-Hermes-Session-Id header, and Calciforge can forward
!model selections when this adapter is explicitly opted in.
[[agents]]
id = "hermes"
kind = "hermes"
endpoint = "http://127.0.0.1:8642"
api_key_file = "~/.config/calciforge/secrets/hermes-api-key"
model = "local-cloud-balanced"
allow_model_override = true
timeout_ms = 600000
kind = "ironclaw"HTTP webhook adapter for IronClaw. Calciforge can route identity and session
metadata to IronClaw, but !model override is not currently forwarded by this
adapter; configure IronClaw's model/provider through IronClaw's own settings or
installer-managed environment.
[[agents]]
id = "ironclaw"
kind = "ironclaw"
endpoint = "http://127.0.0.1:3000"
api_key_file = "~/.config/calciforge/secrets/ironclaw-webhook-secret"
timeout_ms = 300000
kind = "cli"Spawns a command-line subprocess for each message. The command receives the
message via the argument template: {message} in args is replaced at
dispatch time.
Required: command.
[[agents]]
id = "ironclaw"
kind = "cli"
command = "/usr/local/bin/ironclaw"
args = ["run", "-m", "{message}"]
timeout_ms = 60000
env = { "LLM_BACKEND" = "openai_compatible", "LLM_MODEL" = "kimi-k2.5" }
env (optional) — extra environment variables passed to the subprocess.
Security note: {message} in args places user content in the process
argv, which is visible in ps output and /proc/<pid>/cmdline on multi-user
systems. If the message may contain secret values, use a CLI that reads from
stdin instead and pass the message via stdin rather than argv.
kind = "acp"Persistent-session adapter for ACP-compliant agents (e.g. claude --acp,
opencode acp). Unlike cli, the process stays alive between messages so
session context is preserved.
Required: command (the binary to invoke).
[[agents]]
id = "claude-code"
kind = "acp"
command = "claude"
args = ["--acp"]
model = "claude-sonnet-4-5"
timeout_ms = 300000
aliases = ["cc", "claude"]
registry = { display_name = "Claude Code", specialties = ["coding", "refactoring"] }
kind = "acpx"Like acp, but delegates ACP protocol handling to the acpx binary, which
supports additional protocol versions. The command field holds the agent
name (not a path); acpx resolves it.
Required: command (agent name passed to acpx).
Both acpx and the named client command must be installed in the same runtime
that runs Calciforge. If Calciforge runs in Docker, host-level acpx,
opencode, claude, or kilo binaries are not visible unless you build them
into the image or mount a wrapper path. calciforge doctor reports this as a
configuration error.
[[agents]]
id = "opencode"
kind = "acpx"
command = "opencode"
timeout_ms = 300000
kind = "codex-cli" and kind = "dirac-cli"Subprocess adapters for OpenAI Codex CLI and Dirac CLI respectively.
command is optional and defaults to the standard binary name. Both support
model, args, env, and timeout_ms.
[[agents]]
id = "codex"
kind = "codex-cli"
model = "codex-mini-latest"
timeout_ms = 120000
The optional registry table is not used at dispatch time — it populates the
!agents command output so users can discover available agents.
[[agents]]
id = "primary-agent"
kind = "openclaw-channel"
endpoint = "http://127.0.0.1:18789"
api_key_file = "~/.config/calciforge/secrets/primary-agent-token"
timeout_ms = 120000
[agents.registry]
display_name = "Primary Agent"
description = "General-purpose assistant for homelab and daily tasks"
specialties = ["general", "homelab-ops", "research"]
access = ["admin", "user"]
primary_channels = ["telegram", "matrix"]
[[identities]])An identity is a named user. The aliases list maps channel-specific IDs
(phone numbers, Telegram user IDs, Matrix handles) to the identity name.
Routing rules reference the identity id.
| Field | Required | Default | Description |
|---|---|---|---|
id |
yes | — | Unique identity name used in routing rules |
display_name |
no | — | Human-readable name for logs and !who output |
role |
no | — | Arbitrary role string (e.g. "admin", "user") |
aliases |
no | [] |
Per-channel IDs: { channel = "...", id = "..." } |
Alias id format by channel:
| Channel | Alias id format |
Example |
|---|---|---|
telegram |
numeric user ID | "7000000001" |
matrix |
Matrix user ID | "@alice:matrix.org" |
whatsapp |
E.164 phone number | "+15555550001" |
signal |
E.164 phone number | "+15555550001" |
sms |
E.164 phone number | "+15555550001" |
[[identities]]
id = "operator"
display_name = "Alice"
role = "admin"
aliases = [
{ channel = "telegram", id = "7000000001" },
{ channel = "matrix", id = "@alice:matrix.org" },
{ channel = "whatsapp", id = "+15555550001" },
{ channel = "signal", id = "+15555550001" },
]
[[routing]])Each routing rule maps one identity to a default agent and an optional allowlist of agents they may switch to.
| Field | Required | Default | Description |
|---|---|---|---|
identity |
yes | — | Must match an id in [[identities]] |
default_agent |
yes | — | Agent dispatched when no !switch is active |
allowed_agents |
no | [] |
Agents the identity may !switch to; empty = no restriction (any configured agent, regardless of role) |
[[routing]]
identity = "operator"
default_agent = "primary-agent"
allowed_agents = ["primary-agent", "claude-code", "local-llm"]
[[routing]]
identity = "readonly-user"
default_agent = "primary-agent"
allowed_agents = ["primary-agent"]
When allowed_agents is empty, the identity can switch to any configured
agent — there is no role-based check. Set it explicitly for every identity
that should not have unrestricted agent access.
Minimal working config combining agents, identities, and routing:
[calciforge]
version = 2
[[identities]]
id = "operator"
display_name = "Alice"
role = "admin"
aliases = [{ channel = "telegram", id = "7000000001" }]
[[agents]]
id = "primary-agent"
kind = "openclaw-channel"
endpoint = "http://127.0.0.1:18789"
api_key_file = "~/.config/calciforge/secrets/primary-agent-token"
timeout_ms = 120000
[[routing]]
identity = "operator"
default_agent = "primary-agent"
allowed_agents = ["primary-agent"]
[[channels]]
kind = "telegram"
enabled = true
bot_token_file = "~/.config/calciforge/secrets/telegram-token"
calciforge doctor # checks agent reachability and identity/routing consistency
calciforge # start; send a message from a configured alias
calciforge doctor warns or fails on common misconfigurations: missing
api_key on openclaw-channel agents, stale reply callback tokens or hosts,
OpenClaw runtime/model incompatibilities, openai-compat without model,
subprocess agents whose command is not visible to the Calciforge runtime,
identities with no routing rule, and routing rules that reference undefined
agents.