Security Operations
This page covers runtime guardrails, conversation repair logic, security-facing configuration, and dependency choices.
Included Topics
- Loop Guard
- Session Repair
- Security Configuration
- Security Dependencies
Loop Guard
Source: librefang-runtime/src/loop_guard.rs
The LoopGuard tracks tool calls within a single agent loop execution to
detect when the agent is stuck calling the same tool repeatedly.
Configuration
pub struct LoopGuardConfig {
pub warn_threshold: u32, // Default: 3
pub block_threshold: u32, // Default: 5
pub global_circuit_breaker: u32, // Default: 30
}
Detection Algorithm
- For each tool call, compute SHA-256 of
tool_name + "|" + serialized_params. - Increment the count for that hash in a
HashMap<String, u32>. - Increment
total_calls. - Return a graduated verdict:
pub fn check(&mut self, tool_name: &str, params: &serde_json::Value) -> LoopGuardVerdict {
self.total_calls += 1;
// Global circuit breaker
if self.total_calls > self.config.global_circuit_breaker {
return LoopGuardVerdict::CircuitBreak(/* ... */);
}
let hash = Self::compute_hash(tool_name, params);
let count = self.call_counts.entry(hash).or_insert(0);
*count += 1;
if *count >= self.config.block_threshold {
LoopGuardVerdict::Block(/* ... */)
} else if *count >= self.config.warn_threshold {
LoopGuardVerdict::Warn(/* ... */)
} else {
LoopGuardVerdict::Allow
}
}
Verdict Types
| Verdict | Meaning | Action |
|---|---|---|
Allow | Normal operation | Run the tool |
Warn(msg) | Same call repeated >= 3 times | Run, append warning to result |
Block(msg) | Same call repeated >= 5 times | Skip execution, return error |
CircuitBreak(msg) | > 30 total tool calls | Terminate the entire agent loop |
Hash Computation
fn compute_hash(tool_name: &str, params: &serde_json::Value) -> String {
let mut hasher = Sha256::new();
hasher.update(tool_name.as_bytes());
hasher.update(b"|");
let params_str = serde_json::to_string(params).unwrap_or_default();
hasher.update(params_str.as_bytes());
hex::encode(hasher.finalize())
}
Note: serde_json::to_string produces deterministic output (object keys are
sorted), ensuring that semantically identical parameters produce the same hash.
Key Property
Calls with different parameters are tracked separately. An agent that
calls web_search with 10 different queries will not trigger the guard, but
an agent that calls web_search({"query": "test"}) 5 times will be blocked.
Session Repair
Source: librefang-runtime/src/session_repair.rs
Before sending message history to the LLM, this module validates and repairs common structural issues that would cause API errors.
Three-Phase Repair
pub fn validate_and_repair(messages: &[Message]) -> Vec<Message>
Phase 1 -- Collect ToolUse IDs:
Scan all messages for ContentBlock::ToolUse { id, .. } blocks and collect
their IDs into a HashSet<String>.
Phase 2 -- Filter orphans and empties:
- Orphaned ToolResults:
ContentBlock::ToolResult { tool_use_id, .. }blocks wheretool_use_idis not in the ToolUse ID set are dropped. - Empty messages: Messages with empty text or no content blocks are dropped.
Phase 3 -- Merge consecutive same-role messages:
The Anthropic API requires strict role alternation (user, assistant, user, assistant...). If two consecutive messages have the same role, they are merged into a single message with combined content blocks.
Why Each Repair Is Needed
| Issue | Cause | Effect Without Repair |
|---|---|---|
| Orphaned ToolResult | Compaction or truncation removed the ToolUse | API error: "tool_use_id not found" |
| Empty messages | Cancelled generation, empty user submission | API error: empty content |
| Consecutive same-role | Manual history editing, session repair itself | API error: role alternation violation |
Content Merging
When merging consecutive same-role messages, both are converted to block format and concatenated:
fn merge_content(dst: &mut MessageContent, src: MessageContent) {
let dst_blocks = content_to_blocks(std::mem::replace(dst, MessageContent::Text(String::new())));
let src_blocks = content_to_blocks(src);
let mut combined = dst_blocks;
combined.extend(src_blocks);
*dst = MessageContent::Blocks(combined);
}
Security Configuration
config.toml Reference
# API Authentication
api_key = "your-secret-api-key" # Empty = localhost-only mode
# OFP Wire Protocol
[network]
shared_secret = "your-pre-shared-key" # Required for OFP
# WASM Sandbox
[sandbox]
fuel_limit = 1000000 # CPU instruction budget per execution
timeout_secs = 30 # Wall-clock timeout per execution
max_memory_bytes = 16777216 # 16 MB max WASM memory
# Rate Limiting
# 500 tokens/minute/IP (not currently configurable via config.toml)
# Web Search SSRF Protection
[web]
# SSRF protection is always on and cannot be disabled
Environment Variables for Secrets
| Variable | Used By |
|---|---|
OPENAI_API_KEY | OpenAI-compat driver |
ANTHROPIC_API_KEY | Anthropic driver |
GEMINI_API_KEY or GOOGLE_API_KEY | Gemini driver |
DEEPSEEK_API_KEY | DeepSeek provider |
GROQ_API_KEY | Groq provider |
BRAVE_API_KEY | Brave web search |
TAVILY_API_KEY | Tavily web search |
PERPLEXITY_API_KEY | Perplexity web search |
All environment variable API keys are wrapped in Zeroizing<String> when
loaded into driver structs.
Capability Declaration (Agent Manifest)
Capabilities are declared in the agent's TOML manifest:
[agent]
name = "my-agent"
[[capabilities]]
type = "FileRead"
value = "/data/*"
[[capabilities]]
type = "NetConnect"
value = "*.openai.com:443"
[[capabilities]]
type = "ToolInvoke"
value = "web_search"
[[capabilities]]
type = "LlmMaxTokens"
value = 4096
Loop Guard Tuning
The default LoopGuardConfig values are:
| Parameter | Default | Description |
|---|---|---|
warn_threshold | 3 | Identical calls before warning |
block_threshold | 5 | Identical calls before blocking |
global_circuit_breaker | 30 | Total calls before circuit break |
Subprocess Sandbox Allowlists
To pass specific environment variables to subprocesses:
sandbox_command(&mut cmd, &["MY_CUSTOM_VAR".to_string()]);
Only variables explicitly listed in allowed_env_vars (plus the safe
defaults) will be inherited.
Security Dependencies
| Crate | Purpose |
|---|---|
sha2 | SHA-256 hashing (audit trail, loop guard, SSRF, checksums) |
hmac | HMAC-SHA256 for OFP authentication |
hex | Hex encoding/decoding of hashes and signatures |
subtle | Constant-time comparison (ConstantTimeEq) for HMAC verification |
ed25519-dalek | Ed25519 signing/verification for manifest signing |
rand | Cryptographic RNG for key generation (OsRng) |
zeroize | Zeroizing<T> wrapper for automatic secret memory wiping |
governor | GCRA rate limiting algorithm |
wasmtime | WASM sandbox with fuel + epoch metering |
uuid | Nonce generation for OFP handshakes |
chrono | ISO-8601 timestamps for audit entries |
reqwest | HTTP client (used inside SSRF-protected host_net_fetch) |
Why These Specific Crates
- sha2/hmac: Part of the RustCrypto project, audited, widely used in production Rust.
- ed25519-dalek: De facto standard Ed25519 library in Rust, extensively audited.
- subtle: Provides constant-time operations to prevent timing side-channels.
- zeroize: Official RustCrypto approach to zeroing secrets; integrates with
Drop. - governor: Battle-tested GCRA implementation with
DashMap-backed concurrent state.