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

  1. For each tool call, compute SHA-256 of tool_name + "|" + serialized_params.
  2. Increment the count for that hash in a HashMap<String, u32>.
  3. Increment total_calls.
  4. 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

VerdictMeaningAction
AllowNormal operationRun the tool
Warn(msg)Same call repeated >= 3 timesRun, append warning to result
Block(msg)Same call repeated >= 5 timesSkip execution, return error
CircuitBreak(msg)> 30 total tool callsTerminate 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 where tool_use_id is 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

IssueCauseEffect Without Repair
Orphaned ToolResultCompaction or truncation removed the ToolUseAPI error: "tool_use_id not found"
Empty messagesCancelled generation, empty user submissionAPI error: empty content
Consecutive same-roleManual history editing, session repair itselfAPI 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

VariableUsed By
OPENAI_API_KEYOpenAI-compat driver
ANTHROPIC_API_KEYAnthropic driver
GEMINI_API_KEY or GOOGLE_API_KEYGemini driver
DEEPSEEK_API_KEYDeepSeek provider
GROQ_API_KEYGroq provider
BRAVE_API_KEYBrave web search
TAVILY_API_KEYTavily web search
PERPLEXITY_API_KEYPerplexity 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:

ParameterDefaultDescription
warn_threshold3Identical calls before warning
block_threshold5Identical calls before blocking
global_circuit_breaker30Total 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

CratePurpose
sha2SHA-256 hashing (audit trail, loop guard, SSRF, checksums)
hmacHMAC-SHA256 for OFP authentication
hexHex encoding/decoding of hashes and signatures
subtleConstant-time comparison (ConstantTimeEq) for HMAC verification
ed25519-dalekEd25519 signing/verification for manifest signing
randCryptographic RNG for key generation (OsRng)
zeroizeZeroizing<T> wrapper for automatic secret memory wiping
governorGCRA rate limiting algorithm
wasmtimeWASM sandbox with fuel + epoch metering
uuidNonce generation for OFP handshakes
chronoISO-8601 timestamps for audit entries
reqwestHTTP 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.