vulkro triage
Every verdict vulkro triage prints is labelled advisory, non-deterministic,
and excluded from the benchmark. It never changes the deterministic scan
findings, their severities, or the JSON scan output the benchmark consumes:
the benchmark is AI-free. By default the model is a local one served by
Ollama on your own machine, so nothing leaves the box and no API key is
required.
An opt-in, advisory second opinion. vulkro triage runs a normal
deterministic scan, then asks a model for an advisory judgement on the
findings determinism cannot settle on its own:
- the intent-class findings: entitlement / paid-tier bypass, PII / object-property over-exposure, operational reachability of one-off scripts, and source trust
- findings whose reachability the scan could not confirm (verdict
unknownorunreachable): the model re-judges whether a human should still care
The scan engine stays 100% deterministic and calls no model. The advisory verdicts live in a separate report and are never merged back into the scan result.
Usage
vulkro triage [PATH] [FLAGS]
PATH is the project root and defaults to the current directory.
Flags
| Flag | Description | Default |
|---|---|---|
--ai-model <MODEL> | Model tag override (highest precedence). Overrides VULKRO_AI_MODEL, the saved vulkro ai use selection, and the smart default. Example: --ai-model qwen2.5-coder:7b. | (smart default, see below) |
--ai-url <URL> | AI runtime base url override (OpenAI-compatible). A loopback url is allowed under VULKRO_OFFLINE; a cloud url is refused offline. | http://127.0.0.1:11434/v1 |
--model <MODEL> | Legacy alias for --ai-model, kept for back-compat. When both are given, --ai-model wins. | (none) |
--no-ai | Disable the AI layer for this run. vulkro triage then exits 2 with a pointer to vulkro scan, which never needs a model. | off |
--format, -f <FMT> | table (human summary) or json (the advisory verdicts in a labelled envelope). Any other format exits 2. | table |
Model resolution
vulkro triage resolves its model and runtime the same way as every other
AI-layer command, highest precedence first:
- Per-run flags:
--ai-model,--ai-url,--no-ai. - Environment:
VULKRO_AI_MODEL,VULKRO_AI_URL,VULKRO_AI_KEY. - The saved selection from
vulkro ai use(stored in~/.vulkro/scans.db; there are no config files). - The smart default: local Ollama at
http://127.0.0.1:11434/v1withqwen2.5-coder:7b, falling back tophi4-mini:3.8bon a constrained machine (fewer than 4 physical cores). The smart default never selectsqwen2.5-coder:3b, which ships under a non-commercial license.
vulkro ai off turns the AI layer off persistently; a per-run --ai-model
or --ai-url flag (or the matching env var) re-enables it for that run.
vulkro ai setup is the one-time wizard that installs the runtime and
model.
For a local runtime, triage probes once before scanning: it checks the
daemon is up and the model is present (a dead daemon or a missing model is
an actionable error before any scan runs), reads the model's real context
window so a long finding context is not silently truncated, and self-tests
the verdict shape. A model that fails the self-test still runs, with a
stderr note that a 7B model is recommended. When the local model advertises
JSON-schema output, the verdict is requested as a constrained JSON object;
otherwise the plain-text verdict is parsed, and a malformed reply degrades
to an uncertain advisory verdict rather than an error.
Cloud endpoints (bring your own key)
The cloud path still exists as the alternative. Any non-loopback --ai-url
counts as cloud (loopback means localhost, ::1, or 127.0.0.0/8, so an
Ollama box elsewhere on your LAN is classified cloud too):
- A base url containing
anthropic(for examplehttps://api.anthropic.com/v1) routes through the Anthropic Messages client. The key is resolved fromVULKRO_AI_KEY, then the legacyVULKRO_TRIAGE_API_KEY, thenANTHROPIC_API_KEY; with none set the command exits2with a "setVULKRO_TRIAGE_API_KEY" message. Pick the model id with--ai-model. - Any other cloud url uses the generic OpenAI-compatible client;
VULKRO_AI_KEYis sent as a bearer token when set. - A key is never sent to a local endpoint, and no key is ever written to disk.
Environment variables
| Name | Effect |
|---|---|
VULKRO_AI_MODEL | Model tag override. Beaten by --ai-model; beats the saved vulkro ai use selection. |
VULKRO_AI_URL | Runtime base-url override (OpenAI-compatible). Same precedence as VULKRO_AI_MODEL. |
VULKRO_AI_KEY | Cloud API key. Only meaningful for a cloud url: a local endpoint never sends it. Never persisted. |
VULKRO_TRIAGE_API_KEY | Legacy Anthropic key, honoured only on an Anthropic endpoint when VULKRO_AI_KEY is unset. ANTHROPIC_API_KEY is accepted as a further fallback. Not needed for the default local model. |
VULKRO_OFFLINE | Refuses any cloud AI endpoint (exit 2 with guidance) because that would send data off the machine. The default loopback model keeps working under it: nothing leaves the box. The deterministic scan is unaffected either way. |
Exit codes
| Code | Meaning |
|---|---|
0 | Scan completed and no findings needed advisory triage. |
1 | Scan completed and advisory verdicts were reported. |
2 | Error: AI layer off (--no-ai or vulkro ai off), local runtime unreachable or model missing, cloud endpoint under VULKRO_OFFLINE, missing key on an Anthropic endpoint, bad path, or API failure. |
What the output carries
The json output is wrapped in an envelope that makes its status and its
provenance explicit:
{
"advisory": true,
"non_deterministic": true,
"excluded_from_benchmark": true,
"note": "Advisory LLM triage verdicts. These NEVER change the deterministic scan findings, their severities, or the benchmark. Read them as a second opinion only.",
"ai_provider": "ollama",
"ai_model": "qwen2.5-coder:7b",
"ai_endpoint_class": "local",
"total_findings": 0,
"candidates": 0,
"verdicts": []
}
total_findings is the full deterministic count; candidates is how many
findings were handed to the model; verdicts is the advisory array.
ai_provider is ollama, anthropic, or openai-compat, and
ai_endpoint_class is local or cloud, so a pipeline can record after
the fact whether anything left the machine. The deterministic findings are
never present in, or altered by, this document.
Each run is also recorded, best effort, in the project-local .vulkro/ai/
advisory memory store; browse it with vulkro ai log. A failed write
degrades to a note and never affects the run.
Relationship to the scan
vulkro triage is a lens on top of a normal scan, not a replacement for
it. The scan it runs is the same deterministic pipeline every other command
runs. Triage is one surface of the wider opt-in AI layer (alongside
vulkro analyze --ai, vulkro fix --ai, and the vulkro ai commands),
and the same invariant covers all of them: AI assistance is advisory, and
it never changes a deterministic finding, a severity, or the exit code of
vulkro scan. For CI and any reproducible gate, use
vulkro scan; reach for vulkro triage only when you want an
advisory second opinion on the findings determinism cannot judge.
Examples
# One-time setup of the local runtime and model (interactive wizard).
vulkro ai setup
# Advisory triage of the current project on the default local model.
# No API key needed.
vulkro triage .
# Still works air-gapped: a loopback model sends nothing off the box.
VULKRO_OFFLINE=1 vulkro triage .
# JSON verdicts for a pipeline that records the second opinion separately.
vulkro triage . --format json > triage-advisory.json
# Pin a specific local model for this run.
vulkro triage . --ai-model qwen2.5-coder:7b
# Alternative: bring your own key to a cloud Anthropic endpoint.
export VULKRO_AI_KEY=sk-ant-...
vulkro triage . --ai-url https://api.anthropic.com/v1 --ai-model <claude-model-id>
Related
vulkro scan- the deterministic, offline scan that produces the findings triage comments on.- Air-gap: offline mode -
VULKRO_OFFLINEand why a loopback model is allowed under it while any cloud endpoint is refused. - Evidence and AI - the deterministic evidence surfaces an external agent can consume.