Skip to content

AI features

Source of truth: docs/ai.md (synced at build time — edit the original, not this copy). Veltro doesn’t host a model. You bring your own: an OpenAI-compatible endpoint, Anthropic, or a local Ollama daemon. AI is off by default. Nothing reaches a model until an admin saves a provider in Settings > AI and turns AI on.

This page covers what leaves your install once you do, how secrets are masked, what gets audited and how to switch it all off. The engineering detail (provider contract, refusal order, per-product storage) is in AI governance.

Settings > AI needs VectorFlow 0.25.0, Sigmatic 0.49.0 and Warden 0.17.0 or later. Those are the first releases that take their AI settings from identity, and every pin in versions.env is newer. Identity can’t reach a product on an older release: it shows the product as not answering, and “Turn AI off” stays not settled and keeps retrying it without ever turning AI off there. On those releases, set AI up and switch it off on the product’s own Settings > AI page.

Out of the box no product will call a model:

  • VectorFlow stores AI as off (aiEnabled defaults to false).
  • Sigmatic and Warden have no provider stored, so every AI feature refuses with ai_not_configured.
  • The Settings > AI form leaves “Turn AI on when saved” unticked, so you can save a provider without turning anything on.
  • Each product’s list of allowed data classes starts narrow: pipelines in VectorFlow, nothing in Sigmatic or Warden. A Sigmatic install upgraded from before the data-class picker keeps the alerts and events grant it already had.

Settings > AI is one form for every installed product, and only admins can open it.

The form takes a provider (OpenAI-compatible, Anthropic or Ollama), a base URL (optional for Anthropic), a model, an API key, and whether to turn AI on. “Save to every product” sends those values to each installed product at PUT /api/peer/v1/settings/ai as enabled, provider, base_url, model and, if you typed one, api_key. Each product encrypts the key into its own settings store and never returns it; the table on the page only says whether a key is stored.

Leave the key field empty to keep the key each product already has. A product refuses that if you’ve changed the provider or base URL, so a stored key never goes to a host it wasn’t entered for.

Identity holds no AI key. It keeps one row per product in veltro.ai_settings_writes: whether you wanted AI on or off, whether the product confirmed, when, any error, and what the product reported back. That table has no key column. Saving and switching are audited in identity as authority.ai_settings.saved, authority.ai_settings.enabled and authority.ai_settings.disabled.

The “AI everywhere” card has “Turn AI off” and “Turn AI on” buttons and shows on, off or not settled.

“Turn AI off” sends {"enabled": false} to every installed product at once, before it waits on any of them. Then it reads each product back. The switch shows off only when every product has confirmed off. A product that didn’t answer, or answered without applying it, shows as “turning off, retrying” with the reason in the Problem column. Identity keeps retrying it: every 5 seconds while the page is open, and in the background with a backoff from 5 seconds up to 60, which also restarts when identity does. Only turning off is retried. A failed save or turn-on isn’t replayed, because identity doesn’t keep the key.

While AI is off, every AI feature refuses before it calls the provider: Sigmatic and Warden with ai_kill_switch, VectorFlow with ai_disabled. Each refusal is audited. The provider, model and key stay stored, so turning AI back on picks up where you left off.

Each product’s own Settings > AI page keeps the choices that are about that product: which data classes its AI features may send, and in Warden the token, temperature and timeout limits. Identity’s form doesn’t change them.

There are four data classes: alerts, events, cases and pipelines. A feature that needs a class you haven’t allowed is refused with ai_data_class_denied before anything is sent.

Product Feature Data classes sent
VectorFlow VRL writer, pipeline builder, pipeline proposals, cost optimizer, migration translator pipelines
VectorFlow VRL chat pipelines, plus events when you include error context
VectorFlow Pipeline debugger pipelines, plus events when log lines are attached
VectorFlow Schema field-mapping proposals pipelines, events
Sigmatic Rule drafting none: your description and log source hint
Sigmatic Rule explanation none: the rule YAML you give it
Sigmatic Alert summary, alert explanation, attack narrative summary alerts
Sigmatic Exception suggestions, field-mapping suggestions events
Sigmatic Plain-English Explore questions events: the data source’s field names and the time range
Warden Runbook draft none: your description and optional starting YAML
Warden Run explanation alerts, cases
Warden Case triage cases, alerts, events
Warden ai_agent runbook step the classes the step declares (cases if it declares none)

Sigmatic’s and Warden’s connection test sends a fixed probe prompt and no data.

Each product masks secrets in the prompt before it leaves. From VectorFlow 0.24.0, Sigmatic 0.49.0 and Warden 0.17.0 that’s the shared provider package: redact_secrets in veltro-ai (Sigmatic, Warden) and redactSecrets in @veltrosecurity/ai (VectorFlow). A match becomes [REDACTED_<KIND>] and any label in front of it stays, so password=hunter2 goes out as password=[REDACTED_CREDENTIAL]. It masks:

  • Bearer and Basic credentials, and the value of an Authorization or Proxy-Authorization header under the common schemes (Digest, NTLM, Negotiate, AWS SigV4 and others)
  • Signature= values from signed requests and presigned URLs
  • sk- keys (OpenAI, Anthropic), Google AIza keys, GitHub tokens, Slack xox tokens, and AWS AKIA/ASIA access key IDs
  • the value after password, passwd, secret, token, api_key, api_token, credential, aws_secret_access_key and similar keys, written as key=value or key: value
  • JWTs

Earlier releases, including the versions.env defaults above, carry their own copy of the contract with a smaller table. It masks Bearer tokens, AWS AKIA key IDs, JWTs, the value after api_key, token, secret, password or passwd, and sk- keys whose body is only letters and digits. Everything else in the list above, including an sk- key with a - or _ in its body, goes out unmasked on those releases.

The same masking applies to error text written to the audit log.

Redaction matches patterns. A secret in any other shape goes to the model as is: a PEM private key block, say, or a password inside a connection-string URL. So does everything else in the data you allow, like hostnames, usernames and IP addresses. The data-class list is how you control that.

Sigmatic also fences untrusted input. Alert documents, example events and imported rules sit between >>>BEGIN_UNTRUSTED_... and <<<END_UNTRUSTED_... markers, with an instruction to treat what’s inside as data and never follow instructions in it. VectorFlow and Warden prompts don’t fence their input yet.

Model output is a proposal. Drafted rules, VRL, pipelines and runbooks go through the product’s own validator and need a person to save them, and AI can’t close a case or skip an approval.

Every call that reaches a provider writes one ai.invocation row to that product’s own audit log (the AuditLog table in VectorFlow, the hash-chained audit_log in Sigmatic and Warden). The prompt and the answer are never stored.

Field What it holds
product vectorflow, sigmatic or warden
feature Which feature made the call, such as copilot.summarize_alert
model, provider What answered
prompt_digest, output_digest sha256: of the redacted prompt and of the answer
tokens prompt, completion and total
duration_ms How long the call took
actor Who asked, or null for scheduled work
data_classes_used The data classes the prompt carried

A call the provider rejected, or an answer the feature couldn’t use, is still an ai.invocation, because the data left the install.

A call stopped before it reached a provider writes ai.refusal instead: product, feature, refusal_code (ai_kill_switch, ai_disabled, ai_not_configured, ai_data_class_denied and so on), a redacted message, actor and data_classes_used. It has no digests and no tokens, because nothing was sent.

Settings > Audit shows these rows from every product; filter by product to narrow it down. Warden’s SOC metrics report counts token use and refusals from Warden’s own ai.invocation and ai.refusal rows.

The suite ships an optional Compose profile that runs the upstream ollama/ollama:0.6.5 image on loopback. It ships no model and never pulls one by itself:

Terminal window
docker compose -f compose/ollama.yml --profile ollama up -d
docker compose -f compose/ollama.yml --profile ollama exec ollama ollama pull <model>

The second command puts the weights in the ollama-data volume. Which model, and those bytes, are yours.

Started like that, Ollama is its own Compose project on its own network. It’s reachable at http://127.0.0.1:11434 from the host only, and the products can’t resolve the name ollama. compose/ollama.yml isn’t in the root docker-compose.yml include: list on purpose. To let the products reach it as http://ollama:11434, add -f compose/ollama.yml --profile ollama to the suite’s own raw-compose command (the one in the README, with both --env-files, the profiles for your products and SIGMATIC_INGEST_BIND_ADDRESS):

Terminal window
docker compose \
--env-file versions.env --env-file .env \
-f docker-compose.yml -f compose/ollama.yml \
--profile core --profile sigmatic --profile opensearch --profile ollama up -d

That example assumes VELTRO_PRODUCTS=sigmatic in .env; use the profiles that match your products.

Then in Settings > AI pick Ollama, set the base URL to http://ollama:11434 and the model to the one you pulled. Ollama needs no API key. The same base URL goes to every product, and every product’s container is on the suite network, so one Ollama serves them all.

VectorFlow’s strict outbound mode (VF_STRICT_OUTBOUND=true) refuses private and loopback addresses, which rules out a local Ollama. Nothing in this repo sets it.

A local model keeps prompts inside your network. It doesn’t change the rules: the same data classes, redaction, kill switch and audit rows apply.