Skip to content

AI Governance

Source of truth: docs/ai-governance.md (synced at build time — edit the original, not this copy). Versions. The pinned products implement this plane: the veltro-ai-provider/v1 contract, the named refusals, the allow_data_classes gate, the kill switch and the ai.invocation / ai.refusal audit rows are all in the releases versions.env pins. The shared packages described below are in VectorFlow from 0.24.0, Sigmatic from 0.49.0 and Warden from 0.17.0, so every pin has them. From VectorFlow 0.25.0, Sigmatic 0.49.0 and Warden 0.17.0, identity’s Settings > AI sets the provider, model, key and on/off switch for every product (veltro#1150), and VectorFlow’s per-team AI settings and aiKillSwitch are gone. Parts of this page predate that; where it and AI features disagree, AI features is current.

Every AI feature in the Veltro suite is off until an operator configures a model, refuses in a named way when it must not run, and writes an audit row for each attempt — refusals included. AI never closes a case, never bypasses an approval gate, and never reaches a model except through the veltro-ai-provider/v1 contract each product implements.

There is no Veltro-hosted model. Operators bring their own: an OpenAI-compatible endpoint, Anthropic, or a local Ollama daemon. The suite ships no model weights and no bundled model, but it does ship one optional convenience: compose/ollama.yml, an off-by-default profile (profiles: ["ollama"]) that runs the pinned upstream image ollama/ollama:0.6.5 bound to loopback (127.0.0.1:11434) with its own ollama-data volume, no suite secret fan-out, and no pull of its own. The operator opts the profile in (docker compose -f compose/ollama.yml --profile ollama up -d), chooses the model, and runs the pull that puts those weights into the volume, so the model choice and the downloaded bytes stay operator-owned — see docs/ai.md, Local Ollama.

Choosing that profile changes where the model runs, not which rules apply. ollama is a first-class provider value in the same veltro-ai-provider/v1 contract: it requires base_url, needs no api_key_ref (a missing credential refuses only for openai_compatible and anthropic), and it enters the identical gate ladder — kill switch, enabled, configuration, then the allow_data_classes check — with prompts redacted before dispatch and one ai.invocation or ai.refusal row written either way. A local daemon keeps prompt content inside the operator’s network; it does not widen the data classes a feature may use, does not reduce what is audited, and does not bypass the kill switch.

Each product implements the same schema, so one set of rules describes all three:

Field Meaning
provider openai_compatible, anthropic, or ollama
base_url Endpoint. Required for openai_compatible and ollama
model Model name sent to the provider
api_key_ref Opaque handle to a sealed secret. A value that looks like a raw key is rejected
max_tokens, temperature, timeout_seconds Per-request bounds (defaults 2048 / 0.2 / 60)
allow_data_classes Subset of alerts, events, cases, pipelines. Empty denies every context
enabled Product-side opt-in

Implementations:

  • VectorFlow — @veltrosecurity/ai from npm, resolved per team by src/server/services/ai.ts.
  • Sigmatic — veltro-ai from PyPI, dispatched by app/services/ai_copilot.py.
  • Warden — veltro-ai from PyPI, stored and probed by app/services/ai_settings.py.

Each product pins an exact published version of its package, and no product keeps a copy of the contract. Both packages run the shared vectors in conformance/veltro-ai-provider-v1/vectors.json, so the TypeScript and Python clients answer the same config, redaction and structured-output questions the same way. Each package is a library with no service behind it, so a standalone product still reaches its model with no suite coupling.

Configuration is per product, never fanned out. Each product stores its own provider settings and its own sealed key:

Product Storage Settings surface
VectorFlow Team.ai* columns; key encrypted at rest; provider policy on OrganizationSettings Settings → AI
Sigmatic Setting rows ai and ai_kill_switch; keys encrypted at rest Settings → AI, GET /api/settings/ai/status
Warden Setting rows ai and ai_kill_switch; key Fernet-encrypted in the blob Settings → AI, GET /api/settings/ai/status

No API response, log line, prompt, or metric label carries the key. Warden’s public config shape strips api_key_encrypted and returns only the setting:ai/api_key handle plus an api_key_configured boolean; Sigmatic masks a stored key as ********; VectorFlow returns hasApiKey and never the ciphertext.

Only the data classes the operator has allowed, and only for the feature that needs them:

Product Feature Data classes
VectorFlow VRL chat and VRL generation (/api/ai/vrl-chat, /api/ai/vrl) pipelines; pipeline log context only when events is allowed
VectorFlow Pipeline debug assistant (/api/ai/debug) pipelines, plus events for log context
VectorFlow Pipeline generation (/api/ai/pipeline), cost-optimizer enrichment, migration translation pipelines
Sigmatic Rule drafting, alert summary, exception suggestions (/api/ai/copilot/*) alerts and events per the stored allowlist
Sigmatic Field-mapping suggestions events
Sigmatic Plain-English Explore questions (POST /api/ai/copilot/explore-query, ADR-017) events — the selected data source’s field names and the time range
Warden Provider connectivity probe (POST /api/settings/ai/test) none — a fixed two-line prompt
Warden Runbook draft (authoring.draft_runbook) none — the operator’s bounded description and optional base YAML
Warden Run explanation (authoring.explain_run) alerts, cases
Warden Bounded runbook agent (runbook.ai_agent) the data classes declared by the runbook step

A context the operator has not allowed is refused with ai_data_class_denied before any request leaves the process. Widening the allowlist is a settings change by an administrator, recorded in the product’s audit log.

Prompts are redacted before they are sent and before they are logged: bearer tokens, sk-/sk-ant- style keys, AWS access-key IDs, basic-auth credentials, private-key blocks, and connection-string passwords are replaced with a REDACTED marker by the contract’s redact_secrets / redactSecrets helper. Redaction is a mitigation, not a licence: an untrusted document (an alert body, a captured event) is fenced with explicit untrusted-data markers and a standing instruction so that model output is treated as a proposal, never as an instruction to the product.

Every refusal carries a typed code. The three products share these core codes; a product may add feature-level codes such as Warden’s ai_draft_unknown_action and ai_draft_invalid_runbook:

Code Cause
ai_kill_switch The operator kill switch is engaged, or the deployment withdrew AI (demo, suspended)
ai_disabled A provider is configured but switched off
ai_not_configured No provider, or no credential for a provider that needs one
ai_invalid_config Stored settings do not satisfy the contract (including a raw key in api_key_ref)
ai_data_class_denied The feature needs a context the allowlist does not permit
ai_provider_error The provider rejected or failed the request
ai_structured_output_invalid Model output did not satisfy the feature’s schema
ai_timeout The request exceeded timeout_seconds

Refusals are gated in this order, so engaging the kill switch refuses uniformly regardless of how any team has configured a provider:

  1. kill switch (operator switch, or a deployment state that withdraws AI);
  2. product/team opt-in (enabled);
  3. provider configuration and credential;
  4. data-class allowlist;
  5. request bounds and output schema.

Each product has one operator-level AI kill switch, on its Settings → AI page. While engaged:

  • every AI feature refuses with ai_kill_switch — no request reaches the provider;
  • the refusal is audited (see below);
  • stored provider settings are left untouched, so releasing the switch restores exactly the previous state;
  • the settings page says so, and controls that cannot act — such as the connectivity test — are disabled rather than left to fail.
Product Switch Authorisation
VectorFlow OrganizationSettings.aiKillSwitch, settings.updateAiKillSwitch Any administrator may engage it; only the installation owner may release it
Sigmatic Setting key ai_kill_switch, written through PUT /api/settings/ai as kill_switch Administrator
Warden Setting key ai_kill_switch, PUT /api/settings/ai Administrator

VectorFlow’s asymmetry is deliberate: stopping AI must never wait for the owner, and an emergency stop must not be quietly undone by a broader role.

Beyond the switch, VectorFlow also withdraws AI when the deployment is in a demo, suspended or deleted lifecycle state: getOrgConstraints() (src/lib/org-constraints.ts) returns aiEnabled: false with that reason, and getTeamAiConfig refuses as ai_kill_switch carrying the reason (src/server/services/ai.ts), so the refusal is audited like any other. The pinned VectorFlow carries that wiring. VectorFlow also fences custom provider endpoints in two places. At save time, setting a team aiBaseUrl whose host is not api.openai.com, api.anthropic.com or a subdomain of either requires an org OWNER and OrganizationSettings.aiBaseUrlOptIn; that check is unconditional. At call time the same allowlist plus the shared SSRF guard run only under the strict-outbound profile (VF_STRICT_OUTBOUND=true), which rejects loopback, private, mDNS/.internal and cloud-metadata destinations outright — so the local ollama profile is for deployments that do not run strict outbound. The http(s)-scheme check is the one part that runs whether or not strict outbound is on, and it runs at call time (validateBaseUrl in src/server/services/ai.ts, which every AI callsite including cost-optimizer-ai.ts now enters through completeChat); the save-time check resolves the host for the allowlist and does not reject a scheme.

VF_STRICT_OUTBOUND is set in no compose file or env template in this repo, so in every configuration the suite ships the call-time allowlist and SSRF leg are dormant and the save-time OWNER + opt-in gate is what actually fences a custom endpoint.

Each governed AI call produces one provider-contract audit record in the product’s own audit log — VectorFlow’s AuditLog, and Sigmatic’s and Warden’s hash-chained audit_log. Product workflows may also add their own workflow-specific audit event, but the provider record uses one of two action names so the trail does not lie about what happened:

  • ai.invocation — a provider was reached. The row carries product, feature, model, provider, sha256: digests of the redacted prompt and output, tokens (prompt/completion/total), duration_ms, actor, and data_classes_used. Content is never stored. Provider failures and answers rejected by a feature remain invocations because data left the process; their audited failure metadata is redacted, and any tokens the provider returned remain counted.
  • ai.refusal — no provider was reached (kill switch, disabled, not configured, invalid config, denied data class). The row carries product, feature, refusal_code, a redacted message, and the actor. It has no digests and no token object because nothing was sent.

Plain-English query generation (ADR-017) is the one feature row that keeps content: it stores the user’s redacted question and the validated query, so anyone who can read the product’s audit log can read them for as long as audit retention keeps them. The ai.invocation row for the same call still holds digests only.

Filtering the Audit page on these actions shows the governed provider attempts. Sigmatic commits its audit write in a separate transaction. Warden commits the request transaction on refusal/error paths. In both products, the provider attempt remains auditable when the feature returns an error.

  • No auto-close. An AI verdict is recorded on a case or alert as a proposal with a confidence and a rationale. Disposition remains a human action.
  • No bypassed approvals. Any write intent originating from an AI or agent path becomes an approval request on Warden’s normal four-eyes path. An AI-suggested runbook still needs its trigger and its approvals; a bounded agent loop exits at the first write intent and pauses for a human.
  • Proposals, not saves. Drafted Sigma rules, VRL, runbooks, pipelines, and schema mappings are validated by the product’s own validator and shown as a diff. Nothing is persisted without a user action.
  • Registry-bounded. A drafted runbook may only use actions that exist in the registry; a drafted rule must parse as Sigma; malformed structured output is refused with ai_structured_output_invalid rather than partially applied.

Warden’s weekly SOC metrics report and GET /api/metrics/soc compute AI spend from the audited ai.invocation and ai.refusal rows in the report window: provider invocations, refusals broken down by code, prompt/completion/total tokens, and the top features and models by token count. The endpoint remains available to every authenticated dashboard user, but only a global administrator receives installation-wide ai_spend; other roles receive a zeroed spend object while their case and agreement metrics remain team- and private-case-scoped. The audit action determines whether the provider was reached: an ai.invocation whose answer was later refused still contributes its real token usage, while an ai.refusal contributes none. Analyst agreement is computed from ai.triage verdicts and ai_triage_feedback, then exposed as SocMetrics.ai_triage_agreement.

  1. Decide whether AI is allowed at all. Doing nothing leaves it off in all three products.
  2. Choose the model host. A local Ollama daemon keeps prompts inside your network; a vendor endpoint does not.
  3. Set allow_data_classes to the narrowest set that makes the features you want work. Start with pipelines only.
  4. Store the key through the settings page so it is sealed at rest; never place it in an environment variable that logs.
  5. Review the ai.invocation and ai.refusal rows in each product’s Audit page after the first week — features, models, token totals, and any refusals.
  6. Keep the kill switch reachable: confirm who can engage it and who can release it before you need it.