Agents, Identities, and Routing

Status: Implemented

This page covers the three configuration sections that together control who can talk to Calciforge and which AI backend handles their messages:

The 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.

Architecture

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 ([[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.

Common fields

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

Registry metadata

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 ([[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 ([[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.


Full example

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"

Verify

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.