talahon-http 2.2.0

CoPilot HTTP API channel (investigations, SSE chat, status, palace)
Documentation

Talon

The self-hosted AI SOC analyst for the CoPilot SOC stack. Written in Rust · powered by your own vLLM · built to survive hostile input.

CI crates.io docs.rs Rust 1.89+ LLM: vLLM unsafe: forbidden Security review License: MIT

Features · Architecture · Security · Quick start · Deployment · Operations · Docs · Credits


Talon runs next to CoPilot and investigates every alert like a Tier 1/2 analyst: it pulls the raw event from your Wazuh/OpenSearch SIEM, extracts and enriches indicators, maps them to MITRE ATT&CK, correlates across your environment and writes a structured verdict — severity, confidence, evidence and recommended actions — back into CoPilot.

Talon is written in Rust end to end. The model runs on your own vLLM server, so SIEM data never leaves your infrastructure, and every layer assumes that the data it reads was written by an attacker.

✨ Features

🔎 Autonomous investigations

Real-time POST /investigate from CoPilot plus a scheduled sweep as a safety net, deduplicated per alert. Each run ends in a schema-validated verdict, not free text.

🧠 Self-hosted model

A native Rust agent loop talks to vLLM's OpenAI-compatible API: tool calling, streaming, guided JSON decoding, prefix caching. No cloud LLM and no third-party agent framework.

🛡️ Hardened by design

Privilege-separated containers, per-run LLM tokens, prompt-injection fencing, SSRF-safe egress, secret redaction and a hash-chained audit log.

🕵️ Privacy-preserving SIEM access

The anonymizing MCP proxy tokenizes users, hosts, internal IPs and domains, SIDs, MACs and profile paths before results reach the model.

📊 Offline SOC intelligence

Deterministic engine: IOC extraction (16 types, defang-aware), 265-entry ATT&CK mapping, LOLBin and obfuscation analysis with PowerShell decoding, explainable 0–100 triage scoring.

🔁 CoPilot-native

Job status, report and enriched IOCs are written back through CoPilot's REST API. Verdicts and IOC sightings stay local for cross-alert correlation.

At a glance

Runtime Rust workspace: talon on the host, talon-agent in the container
Model vLLM on your own GPUs, or the Anthropic Messages API — both behind the authenticated host gateway
Model credentials The key stays on the host; each run gets a short-lived token over stdin
Container Root supervisor without LLM; MCP servers as separate mcp-* users; agent unprivileged
Untrusted data Nonce-fenced envelopes plus an injection scanner (EN/DE)
Isolation Per lane: every analyst, the investigation lane and the webhook lane are separate principals
Verdicts submit_triage_report with JSON schema, stored and queryable via GET /investigations
Audit SHA-256 hash-chained audit table, talon audit verify

🧩 Architecture

flowchart TB
    ALERT["CoPilot<br/>alert created"]

    subgraph HOST["talon host process"]
        direction LR
        HTTP["talon-http<br/>CoPilot API"]
        CH["talon-channels<br/>Telegram · webhook · Emacs"]
        Q["talon-scheduler<br/>per-lane queue"]
        IPC["talon-ipc<br/>authorized requests"]
        GW["LLM gateway<br/>per-run tokens"]
        DB[("SQLite<br/>verdicts · audit chain")]
    end

    subgraph CT["talon-agent container, one per run"]
        direction LR
        SUP["supervise<br/>root, no LLM"]
        AG["run<br/>unprivileged agent loop"]
        MCP["MCP servers<br/>one mcp-* user each"]
    end

    VLLM["vLLM<br/>your GPUs"]
    SRC["Wazuh · OpenSearch · MySQL<br/>Velociraptor · Shuffle · CVE intel"]
    WB["CoPilot<br/>job · report · IOCs"]

    ALERT -->|"POST /investigate"| HTTP
    HTTP --> Q
    CH --> Q
    Q -->|"docker run, hardened"| SUP
    SUP --> AG
    SUP --> MCP
    AG -->|"unix sockets"| MCP
    AG -->|"run token"| GW
    AG -.->|"IPC files"| IPC
    IPC --> DB
    GW -->|"API key"| VLLM
    MCP --> SRC
    MCP -->|"write-back"| WB

How an investigation runs

sequenceDiagram
    autonumber
    participant C as CoPilot
    participant T as Talon host
    participant A as Agent container
    participant S as SIEM and intel MCPs
    participant L as vLLM
    C->>T: POST /investigate (alert_id, customer_code)
    T->>T: deduplicate, queue on the investigation lane, mint run token
    T->>A: start hardened container, prompt and token over stdin
    A->>S: raw event and index mapping (anonymized)
    A->>A: offline IOC, MITRE and command-line analysis, triage score
    A->>S: threat intel and environment correlation
    A->>L: reason over fenced, untrusted data
    A->>C: write back job, report and IOCs (copilot MCP)
    A->>T: submit_triage_report (schema-validated verdict)
    T->>T: store verdict, revoke token, audit

Each crate lives in crates/talon-* and is published on crates.io as talahon-* (the talon names were taken); its Rust name stays talon_*.

Crate (crates/) crates.io Responsibility
talon talahon Host binary: orchestrator, CLI (run, setup, doctor, audit)
talon-core talahon-core Shared types, host ↔ container protocol, traits, config, scheduling
talon-agent talahon-agent In-container runtime: root supervisor + unprivileged vLLM agent loop
talon-llm talahon-llm vLLM client (streaming, tool-call recovery, guided decoding) + host gateway
talon-mcp talahon-mcp Native Model Context Protocol client, server and manager
talon-tools talahon-tools Built-in agent tools with path guard and SSRF-safe web_fetch
talon-intel talahon-intel IOC / MITRE / command-line / triage / correlation engine + report schema
talon-anon talahon-anon PII anonymizer and talon-anon-proxy
talon-mcp-opensearch talahon-mcp-opensearch Native read-only OpenSearch MCP server talon-opensearch-mcp (opt-in)
talon-security talahon-security Injection guard, redaction, egress, mounts, senders, IPC authz, auth
talon-container talahon-container Hardened docker/podman launcher, mounts, output parser
talon-net talahon-net Hardened HTTP serving: header timeouts, connection caps
talon-scheduler talahon-scheduler Group queue and task scheduler
talon-ipc talahon-ipc Host-side IPC watcher
talon-http talahon-http CoPilot HTTP API channel
talon-channels talahon-channels Telegram, outbound webhook, Emacs bridge
talon-router talahon-router Message formatting and session lanes
talon-db talahon-db SQLite store: messages, tasks, sessions, verdicts, IOC sightings, audit chain
talon-setup talahon-setup Setup, verification, egress firewall and service installation

Design details: docs/RUST_ARCHITECTURE.md.

🔐 Security

Talon treats every alert field, log line, web page and tool result as attacker-controlled. Prompt injection can't be prevented, only contained, so the design assumes a compromised agent in every lane and limits what it can reach.

Layer Defence
Prompt injection Untrusted data is wrapped in randomly nonced <untrusted_data> envelopes the model is told never to obey; a ~75-rule EN/DE scanner (normalization, base64/exfil decoders, tuned to stay quiet on real SIEM telemetry) flags and audits high-risk content
Model credentials The vLLM key stays on the host gateway; each run gets a short-lived token (stored as SHA-256, revoked at the end) over stdin, never env. The gateway pins models, clamps tokens, forwards only allowlisted request fields, salts the prefix cache per lane and rate-limits per run and globally
Container --cap-drop=ALL (+ minimal supervisor caps), no-new-privileges, read-only rootfs + tmpfs, --init, pid/memory/cpu limits, no sudo or node in the image; read-only CLAUDE.md, .mcp.json and MCP dirs; mounts containing the project root, store/, data/, groups/ or .env are refused; credential files in any mount are shadowed
Privilege separation The root supervisor starts each MCP server as its own mcp-* user with a private HOME; the agent runs unprivileged and can't read MCP secrets or their process environments
Lane isolation Every lane is its own principal: it can only message and schedule into itself, touch tasks it owns and never attach scripts; transcripts, reports and archives are per lane
Host ↔ container Identity comes from the IPC directory, never from file contents; openat/O_NOFOLLOW file handling, size and depth limits, claim-before-dispatch, every decision audited
Network Dedicated talon-agents bridge without inter-container traffic; talon setup egress blocks metadata and link-local, exposes only the gateway port on the host and can allowlist MCP destinations
Egress web_fetch and webhooks check every hop against all special IPv4/IPv6 ranges, pin DNS to vetted addresses and refuse redirects to internal hosts
HTTP API Loopback by default, constant-time API key check, per-IP throttling, allowlist CORS, body limits, security headers; header-read timeouts and connection caps on every listener
Memory MemPalace data belongs to mcp-palace; agents can neither read nor plant files there, and host /palace/* calls run in a one-shot locked-down container
Supply chain Base images pinned by digest, every MCP server pinned to a commit SHA or exact version in container/mcp-versions.env, CI actions pinned by SHA; licenses, sources and banned crates enforced with cargo deny; the image is built and smoke-tested in CI
Secrets in output Logs and outbound messages pass through a redactor (bearer/JWT/vendor keys/PEM/basic auth + every configured secret value)
Audit Append-only SQLite audit log with a SHA-256 hash chain; talon audit verify detects tampering

The rewrite went through three rounds of adversarial review; findings, proofs of concept, fixes and accepted residual risks are in docs/MYTHOS_REVIEW.md. Trust model and boundaries: docs/SECURITY.md.

🚀 Quick start

git clone https://github.com/Liohtml/talahon.git /opt/talon && cd /opt/talon
cargo build --release && sudo install -m 0755 target/release/talon /usr/local/bin/talon
./container/build.sh                        # builds talon-agent:latest
cp .env.example .env && chmod 600 .env      # set TALON_VLLM_* and HTTP_API_KEY
talon setup vllm                            # checks that the models are served
talon doctor                                # runtime, image, vLLM, gateway, audit chain
talon run

The host binary is also on crates.io: cargo install talahon --locked installs talon. The agent image, group folders and MCP wrappers still come from the repository checkout.

The deployment guide below covers the mount allowlist, MCP credentials, egress firewall and service installation, which a production setup needs.

📦 Deployment guide

Prerequisites

  • Linux host (macOS works for development) with Docker or Podman
  • Rust 1.89+ (rustup) to build the host binary
  • A vLLM server reachable from the host (docs/PROVIDER_SELECTION.md)
  • OpenSearch/Wazuh SIEM and a CoPilot instance with its MySQL/MariaDB database

1. Build

git clone https://github.com/Liohtml/talahon.git /opt/talon
cd /opt/talon
cargo build --release
sudo install -m 0755 target/release/talon /usr/local/bin/talon
CONTAINER_RUNTIME=docker ./container/build.sh      # builds talon-agent:latest

2. Start vLLM

vllm serve Qwen/Qwen3-32B --enable-auto-tool-choice --tool-call-parser hermes \
  --reasoning-parser qwen3 --enable-prefix-caching --max-model-len 131072 \
  --api-key "$(openssl rand -hex 32)"

Model and parser choices per family are in docs/PROVIDER_SELECTION.md.

[!IMPORTANT] Always run vLLM with --api-key. The host gateway refuses to start without TALON_VLLM_API_KEY (override only with TALON_VLLM_ALLOW_NO_API_KEY=true), because containers could otherwise call an unauthenticated vLLM directly and bypass the gateway.

3. Create .env

cp .env.example .env && chmod 600 .env
# Set at least: TALON_VLLM_BASE_URL, TALON_VLLM_MODEL, TALON_VLLM_MAX_CONTEXT,
#               TALON_VLLM_API_KEY, HTTP_API_KEY
talon setup vllm

4. Create the mount allowlist

The allowlist controls which host directories may be mounted into agent containers. It lives outside the project root in ~/.config/talon/, so agents can't modify it. Without it, no additional mounts are allowed.

[!WARNING] List each directory explicitly — never allowlist the project root or a parent of it. Talon refuses mounts that contain the project root, store/, data/, groups/ or .env, and shadows secret files in every allowed mount.

mkdir -p ~/.config/talon && chmod 700 ~/.config/talon
T=$(pwd)
ro() { printf '    { "path": "%s/%s", "allowReadWrite": false },\n' "$T" "$1"; }
{
  echo '{ "allowedRoots": ['
  for d in siem mysql copilot-mcp wazuh-mcp velociraptor-mcp shuffle-mcp cve-mcp mempalace; do ro "$d"; done
  printf '    { "path": "%s/mempalace-data", "allowReadWrite": true,\n      "description": "MemPalace data, owned by mcp-palace inside containers" }\n' "$T"
  echo '  ],'
  echo '  "blockedPatterns": [],'
  echo '  "nonMainReadOnly": false'
  echo '}'
} > ~/.config/talon/mount-allowlist.json
chmod 600 ~/.config/talon/mount-allowlist.json

MCP directories are always mounted read-only, whatever the allowlist says, and their .env and credential files are shadowed and staged for their mcp-* user only. nonMainReadOnly is false only so that mempalace-data can stay writable. Inside the container it belongs to mcp-palace (mode 0700), so the agent can neither read nor modify it. Host-side /palace/* calls run as mcp-palace (uid 61008) in a one-shot container, so hand the directory over once before the first start and back it up with sudo:

mkdir -p mempalace-data && sudo chown -R 61008:61008 mempalace-data && sudo chmod 700 mempalace-data

5. Configure the MCP servers

Each MCP server keeps its credentials in its own directory. Inside the container they are readable only by that server's user.

Directory Setup Credentials
siem/ bash siem/setup.sh OPENSEARCH_HOSTS, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD; optional TALON_OPENSEARCH_MCP=rust
mysql/ bash mysql/setup.sh MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_DB
copilot-mcp/ bash copilot-mcp/setup.sh COPILOT_URL, COPILOT_USERNAME, COPILOT_PASSWORD
wazuh-mcp/ bash wazuh-mcp/setup.sh WAZUH_PROD_URL, WAZUH_PROD_USERNAME, WAZUH_PROD_PASSWORD
velociraptor-mcp/ bash velociraptor-mcp/setup.sh + api.config.yaml Velociraptor API client config
shuffle-mcp/ (optional) bash shuffle-mcp/setup.sh SHUFFLE_URL, SHUFFLE_API_KEY
cve-mcp/ (optional) bash cve-mcp/setup.sh NVD_API_KEY, optional VirusTotal/Shodan/AbuseIPDB/GreyNoise/urlscan keys
mempalace/ (optional) bash mempalace/setup.sh — (local ChromaDB + SQLite in mempalace-data/)

The versions of all MCP servers are pinned in container/mcp-versions.env, for the image and for the setup.sh scripts alike. To update one, review the upstream diff, change the pin and rebuild the image.

TALON_OPENSEARCH_MCP=rust in siem/.env (opt-in) serves the SIEM tools from the native talon-opensearch-mcp binary (read-only, no Python cold start) instead of the Python opensearch-mcp-server, which stays the default.

Services on the Talon host are reachable from containers as host.docker.internal. Which servers a group uses, and as which user, is defined in groups/<group>/.mcp.json ("runAs": "mcp-siem" etc.).

6. Restrict container egress (Linux)

Agent containers run on the dedicated talon-agents bridge (br-talon, inter-container traffic disabled). The firewall rules always block cloud metadata and link-local addresses, let containers reach the host only on the LLM gateway port and, in allowlist mode, only the destinations your MCP servers need:

talon setup egress --allow 10.0.0.10:9200 --allow 10.0.0.11:3306 \
                   --allow 10.0.0.12:55000 --allow 10.0.0.13:443   # SIEM, MySQL, Wazuh, CoPilot ...
# review the printed nft/iptables script, then:
sudo talon setup egress --apply --allow ...                          # same arguments

Without --allow the rules block private address ranges and keep internet egress (--keep-internet combines both).

[!NOTE] The rules don't survive a reboot on their own. Add --persist to the --apply call (systemd hosts): it installs talon-egress.service, which re-applies the same rules at every boot. --uninstall --apply removes it.

7. Register groups and install the service

talon setup environment                 # runtime, image, vLLM, gateway, disk
talon setup register --jid http:copilot --name CoPilot --folder copilot \
  --trigger @Talon --no-trigger-required
talon setup service                     # systemd user unit (hardened) or launchd plist
talon doctor                            # everything in one report

Manual alternative: systemctl --user enable --now talon with ExecStart=/usr/local/bin/talon run and WorkingDirectory=/opt/talon, or launchd/com.talon.plist on macOS.

8. Verify

curl http://localhost:3100/health                          # unauthenticated
export TALON_API_KEY=...                                    # HTTP_API_KEY from .env
curl -s -N -X POST http://localhost:3100/message \
  -H "Content-Type: application/json" -H "x-api-key: $TALON_API_KEY" \
  -d '{"message": "Check SIEM cluster health", "sender": "test"}'
curl -s -H "x-api-key: $TALON_API_KEY" http://localhost:3100/status
talon audit tail -n 20

The API binds 127.0.0.1 by default. Set COPILOT_HTTP_BIND to a private interface if CoPilot runs on another host, and terminate TLS in front of it.

🧰 Operations

Command Purpose
talon run Run the service (default)
talon doctor Diagnose runtime, image, vLLM, gateway, directories, audit chain
talon setup <step> timezone, environment, vllm, container, groups, register, mounts, egress, service, verify
talon audit verify Recompute the audit hash chain (exit code 1 on tampering)
talon audit tail -n 50 --kind ipc. Recent audit events as JSON lines
talon eval score --reports <dir> Score saved verdicts against the golden set (offline)
talon eval run Investigate every golden case through a running Talon, then score

Logs: RUST_LOG=info (JSON with TALON_LOG_FORMAT=json); every line passes through the redactor.

Investigation quality (talon eval)

evals/golden/ holds synthetic alerts with a known expected outcome: verdict, severity range, indicators and ATT&CK techniques that must be reported, and content that must not appear (invented indicators, leaked anonymization tokens such as USER_1). Run it before and after a prompt, model or provider change.

# Offline: <dir>/<case id>.json is a verdict (TriageReport) or a
# GET /investigations/{id} record.
talon eval score --reports <dir> [--json] [--min-pass-rate 0.85]

# Online: POST /investigate per case on a running Talon, wait, score.
talon eval run --alert-map map.json --save-dir out/ [--url http://127.0.0.1:3100]

Both print a per-case table, the failed checks and a verdict confusion matrix (--json for the same as JSON), and exit 1 when the share of passing cases is below --min-pass-rate (default 1.0), 2 on unusable input. run needs the cases' alerts seeded in the CoPilot stack the instance is connected to and performs real investigations with all their write-backs, so point it at a dedicated eval instance with COPILOT_INVESTIGATION_DEDUP_MINUTES=0; see the golden-set README.

HTTP API

All endpoints except /health require X-Api-Key or Authorization: Bearer.

Endpoint Purpose
POST /investigate {alert_id, customer_code, triggered_by?, template_override?} → queued investigation, deduplicated per alert; triggered_by is manual, scheduled or webhook (default)
GET /investigations, GET /investigations/{id} Investigation status and structured verdicts
POST /message Analyst chat (SSE stream), per-user lanes via user_id
POST /session/reset {scope, user_id?} with scope chat, investigate or all
GET /status, GET /metrics Queue, lanes, sessions, tasks; Prometheus metrics
GET /templates, GET /evals/{alert_id} Investigation templates and eval records
POST /palace/lesson, GET /palace/search, POST /palace/forget MemPalace memory

Per-deployment configuration

File Purpose
.env vLLM, gateway, HTTP API key, channels — chmod 600, gitignored
<mcp-dir>/.env Per-MCP credentials — gitignored
groups/copilot/CLAUDE.md SOC agent identity and investigation workflow (read-only inside containers); add client context such as assets, known-good ranges and crown jewels at the bottom
groups/<group>/MEMORY.md The group's own agent-written notes (shown to the agent as untrusted data)
groups/copilot/.mcp.json MCP servers and their runAs users (mounted read-only)
groups/copilot/prompts/ Per-alert-type investigation templates — add .txt files, no code changes
siem/anon_proxy/fields.yaml PII field definitions for the anonymizing proxy
~/.config/talon/mount-allowlist.json Mount policy — outside the repo, tamper-proof
~/.config/talon/sender-allowlist.json Optional per-chat sender allowlist (TALON_SENDER_ALLOWLIST_REQUIRED=true to fail closed)
mempalace-data/ MemPalace data — back it up separately

🔧 Development

cargo build --workspace
cargo test --workspace                                   # 2,100+ tests
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
./container/build.sh                                     # agent image

Codebase rules: no unsafe, no unwrap/expect outside tests, untrusted data fenced with talon_security::injection::fence_untrusted, security decisions audited via talon_core::audit. See CONTRIBUTING.md.

Releases

Every merge to main that passes CI and changes a crate is released automatically (release.yml): the next version follows Conventional Commits (feat → minor, type!: / BREAKING CHANGE: → major, anything else → patch), CHANGELOG.md gets the entry, main gets a chore(release) commit and a vX.Y.Z tag, GitHub gets a release, and all talahon-* crates are published to crates.io with the same version.

📚 Documentation

Document Contents
docs/RUST_ARCHITECTURE.md Architecture, crate map, security contracts
docs/SECURITY.md Trust model, boundaries, residual risks, reporting
docs/MYTHOS_REVIEW.md Adversarial security review: findings and fixes
docs/PROVIDER_SELECTION.md vLLM setup, model and parser choice, gateway settings
docs/COPILOT_INTEGRATION.md CoPilot integration, MySQL schema, MCP tool reference
docs/ANON_PROXY.md Anonymizing proxy: flow, token types, fields.yaml
groups/copilot/prompts/README.md Writing investigation templates
CHANGELOG.md Release notes

🙏 Credits

Talon 2.0 is an independent Rust rewrite, but it stands on the work of others:

What the rewrite changed

The original Talon is a Node.js/TypeScript fork of NanoClaw that drives the Claude Agent SDK; its repository is no longer maintained. Talon 2.0 keeps its investigation workflow and replaces the runtime:

Original Talon Talon 2.0
Runtime Node.js host, Claude Agent SDK in the container One Rust workspace: host, in-container agent loop, LLM gateway, anonymizer
Model Claude by default, Ollama through the SDK vLLM on your own GPUs by default, Anthropic optional — always behind the host gateway
Model credentials Credential proxy, with an env-var fallback into the container Key never leaves the host; a per-run token over stdin
Anonymizing proxy Python Rust, with more PII coverage (lists, nested and dotted fields, free text)

New in the rewrite: privilege separation inside the container (root supervisor without LLM, every MCP server as its own user), per-lane isolation, the deterministic IOC / ATT&CK / triage engine with schema-validated verdicts, the hash-chained audit log, and three rounds of adversarial review.

📄 License

Released under the MIT License.