# 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`](../crates/runtime/src/http_input/config.rs) structure:

### Basic Configuration

```rust
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:

```rust
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`](../crates/runtime/src/http_input/config.rs):

```rust
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:

```rust
let config = HttpInputConfig {
    auth_header: Some("Bearer your-secret-token".to_string()),
    ..Default::default()
};
```

#### Secret Store Integration

Use secret references for enhanced security:

```rust
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:

```rust
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.

**Algorithm allowlist (post-v1.13.0 audit):** the asymmetric Bearer-token path accepts only **ES256** and **EdDSA**. The HMAC webhook-signature path (used by Slack, Mattermost, GitHub etc.) accepts only **HS256**. RSA-family algorithms (`RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`) and the symbolic `none` algorithm are refused at both the header-inspection guard and the `Validation::algorithms` allowlist. This neutralizes the `rsa` crate timing-attack advisory (RUSTSEC-2023-0071) on every path the operator controls. See `SECURITY_AUDIT.md` C4. (The Microsoft Teams adapter still uses RS256 because the Bot Framework requires it; that surface is bounded to MS-signed tokens.)

**Audience is required:** the `SYMBIONT_ALLOW_NO_JWT_AUDIENCE` env-var escape hatch was removed in the post-v1.13.0 audit. Every JWT verifier requires an explicit `aud` configuration; tokens without a matching audience 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:

```bash
# 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_address` defaults to `127.0.0.1` — the server only accepts local connections unless explicitly configured otherwise
- **CORS Disabled by Default**: `cors_origins` defaults to an empty list, meaning CORS is disabled; add specific origins to enable cross-origin access. A literal `"*"` in `cors_origins` is **rejected at startup** — the HTTP Input server will refuse to start with a wildcard origin. (Added in the post-v1.13.0 audit; see `SECURITY_AUDIT.md` M1.)
- **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

```rust
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.symbi`](../agents/webhook_handler.symbi):

```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:

```bash
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:

```json
{
  "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](#llm-invocation-with-toolclad-tools) below). The response includes the final text and a summary of any tool calls that were executed:

```json
{
  "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

1. 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, since `send_message` would silently drop them.
2. If the agent is not running, the handler builds a system prompt from any `.symbi` (or legacy `.dsl`) agent files found in the `agents/` directory, appends an optional caller-supplied `system_prompt` (length-capped and logged), and constructs a user message from the request payload.
3. ToolClad manifests in the `tools/` directory are loaded and exposed to the LLM as function-calling tools. Custom types from `toolclad.toml` are applied.
4. The handler runs an **ORGA** (Observe-Reason-Gate-Act) tool-calling loop, up to 15 iterations:
   - The LLM proposes zero or more `tool_use` calls.
   - 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_result` messages.
   - The loop terminates when the LLM produces a final text response or the iteration cap is reached.
5. 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:

- `prompt` or `message` — 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:

```rust
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:

```rust
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](#health-endpoint) section above for details.

## Error Handling

The HTTP Input module provides comprehensive error handling:

- **Authentication Errors**: Returns `401 Unauthorized` for invalid tokens
- **Rate Limiting**: Returns `429 Too Many Requests` when concurrency limits are exceeded
- **Payload Errors**: Returns `400 Bad Request` for malformed JSON
- **Agent Errors**: Returns configurable error status with error details
- **Server Errors**: Returns `500 Internal Server Error` for runtime failures

## Monitoring and Observability

### Audit Logging

When `audit_enabled` is true, the module logs structured information about all requests:

```log
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:

```log
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

1. **Security**: Always use authentication in production environments
2. **Rate Limiting**: Configure appropriate concurrency limits based on your infrastructure
3. **Monitoring**: Enable audit logging and integrate with your monitoring stack
4. **Error Handling**: Configure appropriate error responses for your use case
5. **Agent Design**: Design agents to handle webhook-specific input formats
6. **Resource Limits**: Set reasonable body size limits to prevent resource exhaustion

## See Also

- [Getting Started Guide](getting-started.md)
- [DSL Guide](dsl-guide.md)
- [API Reference](api-reference.md)
- [Reasoning Loop (ORGA)](reasoning-loop.md)
- [ToolClad Tool Contracts](toolclad.md)
- [Agent Runtime Documentation](../crates/runtime/README.md)