HTTP Input Module¶
The HTTP Input module provides a webhook server that allows external systems to invoke Symbiont agents via HTTP requests. This module enables integration with external services, webhooks, and APIs by exposing agents through HTTP endpoints.
Overview¶
The HTTP Input module consists of:
- HTTP Server: An Axum-based web server that listens for incoming HTTP requests
- Authentication: Support for Bearer token and JWT-based authentication
- Request Routing: Flexible routing rules to direct requests to specific agents
- Response Control: Configurable response formatting and status codes
- Security Features: CORS support, request size limits, and audit logging
- Concurrency Management: Built-in request rate limiting and concurrency control
- LLM Invocation with ToolClad: When the target agent is not actively running on the runtime communication bus, the webhook can invoke the agent on-demand via a configured LLM provider, using an ORGA-style tool-calling loop backed by ToolClad manifests
The module is conditionally compiled with the http-input feature flag and integrates seamlessly with the Symbiont agent runtime.
Configuration¶
The HTTP Input module is configured using the HttpInputConfig structure:
Basic Configuration¶
use symbiont_runtime::http_input::HttpInputConfig;
use symbiont_runtime::types::AgentId;
let config = HttpInputConfig {
bind_address: "127.0.0.1".to_string(),
port: 8081,
path: "/webhook".to_string(),
agent: AgentId::from_str("webhook_handler")?,
// ... other fields
..Default::default()
};
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
bind_address |
String |
"127.0.0.1" |
IP address to bind the HTTP server |
port |
u16 |
8081 |
Port number to listen on |
path |
String |
"/webhook" |
HTTP path endpoint |
agent |
AgentId |
New ID | Default agent to invoke for requests |
auth_header |
Option<String> |
None |
Bearer token for authentication |
jwt_public_key_path |
Option<String> |
None |
Path to JWT public key file |
max_body_bytes |
usize |
65536 |
Maximum request body size (64 KB) |
concurrency |
usize |
10 |
Maximum concurrent requests |
routing_rules |
Option<Vec<AgentRoutingRule>> |
None |
Request routing rules |
response_control |
Option<ResponseControlConfig> |
None |
Response formatting config |
forward_headers |
Vec<String> |
[] |
Headers to forward to agents |
cors_origins |
Vec<String> |
[] |
Allowed CORS origins (empty = CORS disabled) |
audit_enabled |
bool |
true |
Enable request audit logging |
Agent Routing Rules¶
Route requests to different agents based on request characteristics:
use symbiont_runtime::http_input::{AgentRoutingRule, RouteMatch};
let routing_rules = vec![
AgentRoutingRule {
condition: RouteMatch::PathPrefix("/api/github".to_string()),
agent: AgentId::from_str("github_handler")?,
},
AgentRoutingRule {
condition: RouteMatch::HeaderEquals("X-Source".to_string(), "slack".to_string()),
agent: AgentId::from_str("slack_handler")?,
},
AgentRoutingRule {
condition: RouteMatch::JsonFieldEquals("source".to_string(), "twilio".to_string()),
agent: AgentId::from_str("sms_handler")?,
},
];
Response Control¶
Customize HTTP responses with ResponseControlConfig:
use symbiont_runtime::http_input::ResponseControlConfig;
let response_control = ResponseControlConfig {
default_status: 200,
agent_output_to_json: true,
error_status: 500,
echo_input_on_error: false,
};
Security Features¶
Authentication¶
The HTTP Input module supports multiple authentication methods:
Bearer Token Authentication¶
Configure a static bearer token:
let config = HttpInputConfig {
auth_header: Some("Bearer your-secret-token".to_string()),
..Default::default()
};
Secret Store Integration¶
Use secret references for enhanced security:
let config = HttpInputConfig {
auth_header: Some("vault://webhook/auth_token".to_string()),
..Default::default()
};
JWT Authentication (EdDSA)¶
Configure JWT-based authentication with Ed25519 public keys:
let config = HttpInputConfig {
jwt_public_key_path: Some("/path/to/jwt/ed25519-public.pem".to_string()),
..Default::default()
};
The JWT verifier loads an Ed25519 public key from the specified PEM file and validates incoming Authorization: Bearer <jwt> tokens. Only the EdDSA algorithm is accepted — HS256, RS256, and other algorithms are rejected.
Health Endpoint¶
The HTTP Input module does not expose its own /health endpoint. Health checks are available via the main HTTP API at /api/v1/health when running symbi up, which starts the full runtime including the API server:
# Health check via the main API server (default port 8080)
curl http://127.0.0.1:8080/api/v1/health
# => {"status": "ok"}
If you need health probes for the HTTP Input server specifically, route your load balancer to the main API health endpoint instead.
Security Controls¶
- Loopback-Only Default:
bind_addressdefaults to127.0.0.1— the server only accepts local connections unless explicitly configured otherwise - CORS Disabled by Default:
cors_originsdefaults to an empty list, meaning CORS is disabled; add specific origins to enable cross-origin access - Request Size Limits: Configurable maximum body size prevents resource exhaustion
- Concurrency Limits: Built-in semaphore controls concurrent request processing
- Audit Logging: Structured logging of all incoming requests when enabled
- Secret Resolution: Integration with Vault and file-based secret stores
Usage Example¶
Starting the HTTP Input Server¶
use symbiont_runtime::http_input::{HttpInputConfig, start_http_input};
use symbiont_runtime::secrets::SecretsConfig;
use std::sync::Arc;
// Configure the HTTP input server
let config = HttpInputConfig {
bind_address: "127.0.0.1".to_string(),
port: 8081,
path: "/webhook".to_string(),
agent: AgentId::from_str("webhook_handler")?,
auth_header: Some("Bearer secret-token".to_string()),
audit_enabled: true,
cors_origins: vec!["https://example.com".to_string()],
..Default::default()
};
// Optional: Configure secrets
let secrets_config = SecretsConfig::default();
// Start the server
start_http_input(config, Some(runtime), Some(secrets_config)).await?;
Example Agent Definition¶
Create a webhook handler agent in webhook_handler.dsl:
agent webhook_handler(body: JSON) -> Maybe<Alert> {
capabilities = ["http_input", "event_processing", "alerting"]
memory = "ephemeral"
privacy = "strict"
policy webhook_guard {
allow: use("llm") if body.source == "slack" || body.user.ends_with("@company.com")
allow: publish("topic://alerts") if body.type == "security_alert"
audit: all_operations
}
with context = {} {
if body.type == "security_alert" {
alert = {
"summary": body.message,
"source": body.source,
"level": body.severity,
"user": body.user
}
publish("topic://alerts", alert)
return alert
}
return None
}
}
Example HTTP Request¶
Send a webhook request to trigger the agent:
curl -X POST http://localhost:8081/webhook \
-H "Content-Type: application/json" \
-H "Authorization: Bearer secret-token" \
-d '{
"type": "security_alert",
"message": "Suspicious login detected",
"source": "slack",
"severity": "high",
"user": "admin@company.com"
}'
Expected Response¶
The response shape depends on how the agent was invoked.
Runtime dispatch — the target agent is Running on the communication bus and the message was handed off for asynchronous processing:
{
"status": "execution_started",
"agent_id": "webhook_handler",
"message_id": "01H...",
"latency_ms": 3,
"timestamp": "2024-01-15T10:30:00Z"
}
LLM invocation — the agent is not running and was executed on-demand via the configured LLM provider (see LLM Invocation with ToolClad Tools below). The response includes the final text and a summary of any tool calls that were executed:
{
"status": "completed",
"agent_id": "webhook_handler",
"response": "Scanned target and found 3 open ports …",
"tool_runs": [
{
"tool": "nmap_scan",
"input": {"target": "example.com"},
"output_preview": "{\"scan_id\": \"…\", \"ports\": [ … ]}"
}
],
"model": "claude-sonnet-4-20250514",
"provider": "Anthropic",
"latency_ms": 4821,
"timestamp": "2024-01-15T10:30:00Z"
}
LLM Invocation with ToolClad Tools¶
When the runtime is attached but the routed agent is not in the Running state, the webhook handler falls through to an on-demand LLM invocation path. This is useful for agents that execute per-request rather than as long-running listeners.
How it works¶
- The webhook handler calls
scheduler.get_agent_status()to verify the agent is actively running. Messages to non-running agents are not dispatched via the communication bus, sincesend_messagewould silently drop them. - If the agent is not running, the handler builds a system prompt from any
.dslfiles found in theagents/directory, appends an optional caller-suppliedsystem_prompt(length-capped and logged), and constructs a user message from the request payload. - ToolClad manifests in the
tools/directory are loaded and exposed to the LLM as function-calling tools. Custom types fromtoolclad.tomlare applied. - The handler runs an ORGA (Observe-Reason-Gate-Act) tool-calling loop, up to 15 iterations:
- The LLM proposes zero or more
tool_usecalls. - Each tool call is validated by ToolClad and executed on a blocking thread pool with a 120-second per-tool timeout.
- Duplicate
(tool_name, input)pairs within a single iteration are deduplicated to avoid redundant execution of non-idempotent tools. - Tool results are fed back to the LLM as
tool_resultmessages. - The loop terminates when the LLM produces a final text response or the iteration cap is reached.
- The final response, the list of executed tool runs, and provider/model metadata are returned to the caller.
Provider auto-detection¶
The LLM client is initialized from environment variables at server start. The first provider whose API key is set wins, in this order:
| Env var | Provider | Model override | Base URL override |
|---|---|---|---|
OPENROUTER_API_KEY |
OpenRouter | OPENROUTER_MODEL (default: anthropic/claude-sonnet-4) |
OPENROUTER_BASE_URL |
OPENAI_API_KEY |
OpenAI | CHAT_MODEL (default: gpt-4o) |
OPENAI_BASE_URL |
ANTHROPIC_API_KEY |
Anthropic | ANTHROPIC_MODEL (default: claude-sonnet-4-20250514) |
ANTHROPIC_BASE_URL |
If no API key is set, the LLM invocation path is disabled and requests for non-running agents return an error.
Input fields¶
The webhook JSON body is interpreted as follows when the LLM path is taken:
promptormessage— used as the user message. If neither is present, the whole payload is pretty-printed and passed as the task description.system_prompt— optional caller-supplied system prompt appended to the DSL-derived system prompt. Capped at 4096 bytes and logged. Treat as a prompt-injection surface: always enforce authentication when exposing this endpoint to untrusted callers.
Normalized tool-call format¶
The LLM client normalizes OpenAI/OpenRouter function calling into the same content-block shape used by the Anthropic Messages API. Regardless of provider, each response content block is either {"type": "text", "text": "..."} or {"type": "tool_use", "id": "...", "name": "...", "input": {...}}, and stop_reason is "end_turn" or "tool_use".
Integration Patterns¶
Webhook Endpoints¶
Configure different agents for different webhook sources:
let routing_rules = vec![
AgentRoutingRule {
condition: RouteMatch::HeaderEquals("X-GitHub-Event".to_string(), "push".to_string()),
agent: AgentId::from_str("github_push_handler")?,
},
AgentRoutingRule {
condition: RouteMatch::JsonFieldEquals("source".to_string(), "stripe".to_string()),
agent: AgentId::from_str("payment_processor")?,
},
];
API Gateway Integration¶
Use as a backend service behind an API gateway:
let config = HttpInputConfig {
bind_address: "0.0.0.0".to_string(),
port: 8081,
path: "/api/webhook".to_string(),
cors_origins: vec!["https://example.com".to_string()],
forward_headers: vec![
"X-Forwarded-For".to_string(),
"X-Request-ID".to_string(),
],
..Default::default()
};
Health Check Integration¶
The HTTP Input module does not include a dedicated health endpoint. Use the main API health endpoint (/api/v1/health) for load balancer and monitoring integration. See the Health Endpoint section above for details.
Error Handling¶
The HTTP Input module provides comprehensive error handling:
- Authentication Errors: Returns
401 Unauthorizedfor invalid tokens - Rate Limiting: Returns
429 Too Many Requestswhen concurrency limits are exceeded - Payload Errors: Returns
400 Bad Requestfor malformed JSON - Agent Errors: Returns configurable error status with error details
- Server Errors: Returns
500 Internal Server Errorfor runtime failures
Monitoring and Observability¶
Audit Logging¶
When audit_enabled is true, the module logs structured information about all requests:
INFO HTTP Input: Received request with 5 headers
INFO Agent webhook_handler is running, dispatching via communication bus
INFO Runtime execution dispatched for agent webhook_handler: message_id=… latency=3ms
When the LLM invocation path is used, additional lines trace the ORGA loop:
INFO Agent webhook_handler is not running, using LLM invocation path
INFO Invoking LLM for agent webhook_handler: provider=Anthropic model=… tools=4 …
INFO ORGA ACT: executing tool 'nmap_scan' (id=…) for agent webhook_handler
INFO Tool 'nmap_scan' executed successfully
INFO ORGA loop iteration 1 for agent webhook_handler: executed 1 tool(s), continuing
INFO LLM invocation completed for agent webhook_handler: latency=4821ms tool_runs=1 response_len=…
Metrics Integration¶
The module integrates with the Symbiont runtime's metrics system to provide:
- Request count and rate
- Response time distributions
- Error rates by type
- Active connection counts
- Concurrency utilization
Best Practices¶
- Security: Always use authentication in production environments
- Rate Limiting: Configure appropriate concurrency limits based on your infrastructure
- Monitoring: Enable audit logging and integrate with your monitoring stack
- Error Handling: Configure appropriate error responses for your use case
- Agent Design: Design agents to handle webhook-specific input formats
- Resource Limits: Set reasonable body size limits to prevent resource exhaustion