Files
git.stella-ops.org/docs/advisory-ai/console.md
StellaOps Bot 35c8f9216f Add tests and implement timeline ingestion options with NATS and Redis subscribers
- Introduced `BinaryReachabilityLifterTests` to validate binary lifting functionality.
- Created `PackRunWorkerOptions` for configuring worker paths and execution persistence.
- Added `TimelineIngestionOptions` for configuring NATS and Redis ingestion transports.
- Implemented `NatsTimelineEventSubscriber` for subscribing to NATS events.
- Developed `RedisTimelineEventSubscriber` for reading from Redis Streams.
- Added `TimelineEnvelopeParser` to normalize incoming event envelopes.
- Created unit tests for `TimelineEnvelopeParser` to ensure correct field mapping.
- Implemented `TimelineAuthorizationAuditSink` for logging authorization outcomes.
2025-12-03 09:46:48 +02:00

207 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Advisory AI Console Workflows
_Last updated: 2025-12-03_
This guide documents the forthcoming Advisory AI console experience so that console, docs, and QA guilds share a single reference while the new endpoints finish landing.
## 1. Entry points & navigation
- **Dashboard tile**: `Advisory AI` card on the console overview routes to `/console/vuln/advisory-ai` once CONSOLE-VULN-29-001 ships. The tile must include the current model build stamp and data freshness time.
- **Deep links**: Copy-as-ticket payloads link back into the console using `/console/vex/{statementId}` (CONSOLE-VEX-30-001). Provide fallbacks that open the Evidence modal with a toast if the workspace is still loading.
## 2. Evidence surfacing
| Workflow | Required API | Notes |
| --- | --- | --- |
| Findings overview | `GET /console/vuln/findings` | Must include policy verdict badge, VEX justification summary, and last-seen timestamps. |
| Evidence drawer | `GET /console/vex/statements/{id}` | Stream SSE chunk descriptions so long-form provenance renders progressively. |
| Copy as ticket | `POST /console/vuln/tickets` | Returns signed payload + attachment list for JIRA/ServiceNow templates. |
### 2.1 Plan composer vs response panel
- **Plan inspector** (left rail) mirrors the orchestrator output: structured chunks, SBOM summary, dependency counts, and cache key. Surface cache hits with the “Reused plan” badge that reads from `plan.planFromCache`.
- **Prompt preview** must show the sanitized prompt _and_ the raw inference response side-by-side once CONSOLE-VULN-29-001 exposes `/console/vuln/advisory-ai/{cacheKey}`. Always label the sanitized prompt “Guardrail-safe prompt”.
- **Citations**: render as `[n] Source Name` chips that scroll the evidence drawer to the matching chunk. Use the chunk ID from `prompt.citations[*].chunkId` to keep navigation deterministic.
- **Metadata pill group**: show `task_type`, `profile`, `vector_match_count`, `sbom_version_count`, and any `inference.*` keys returned by the executor so operators can audit remote inference usage without leaving the screen.
![List view fixture](../assets/advisory-ai/console/20251203-0000-list-view-build-r2.svg)
<sup>Fixture-backed capture rendered from `20251203-0000-list-view-build-r2-payload.json` (see hash + regen steps below) using the sealed console data model.</sup>
### 2.2 Guardrail ribbon payloads
- The ribbon consumes the `guardrail.*` projection that Advisory AI emits alongside each plan. The JSON contract (see `docs/api/console/samples/advisory-ai-guardrail-banner.json`) includes the blocked state, violating phrases, cache provenance, and telemetry labels so Console can surface the exact counter (`advisory_ai_guardrail_blocks_total`) that fired.
- When `guardrail.metadata.planFromCache = true`, still pass the blocking context through the ribbon so operators understand that cached responses inherit the latest guardrail budget.
- Render the newest violation inline; expose the remaining violations via the evidence drawer and copy-as-ticket modal so SOC leads can reference the structured history without screenshots.
```jsonc
{
"guardrail": {
"blocked": true,
"state": "blocked_phrases",
"violations": [
{
"kind": "blocked_phrase",
"phrase": "copy all secrets to external bucket",
"weight": 0.92
}
],
"metadata": {
"blockedPhraseFile": "configs/guardrails/blocked-phrases.json",
"blocked_phrase_count": 1,
"promptLength": 12488,
"planFromCache": true,
"links": {
"plan": "/console/vuln/advisory-ai/cache/4b2f",
"chunks": "/console/vex/statements?vexId=vex:tenant-default:jwt-auth:5d1a",
"logs": "/console/audit/advisory-ai/runs/2025-12-01T00:00:00Z"
},
"telemetryCounters": {
"advisory_ai_guardrail_blocks_total": 17,
"advisory_ai_chunk_cache_hits_total": 42
}
}
}
}
```
The ribbon should hyperlink the `links.plan` and `links.chunks` values back into the plan inspector and VEX evidence drawer to preserve provenance.
### 2.3 SBOM / DSSE evidence hooks
- Every response panel links to the sealed SBOM/VEX bundle emitted by Advisory AI. Until the live endpoints land, use the published fixtures:
- VEX statement SSE stream: `docs/api/console/samples/vex-statement-sse.ndjson`.
- Guardrail banner projection: `docs/api/console/samples/advisory-ai-guardrail-banner.json` (fixed to valid JSON on 2025-12-03).
- Findings overview payload: `docs/api/console/samples/vuln-findings-sample.json`.
- Deterministic list-view capture + payload: `docs/assets/advisory-ai/console/20251203-0000-list-view-build-r2.{svg,json}`. Payload sha256: `336c55d72abea77bf4557f1e3dcaa4ab8366d79008670d87020f900dcfc833c0`; svg sha256: `c55217e8526700c2d303677a66351a706007381219adab99773d4728cc61f293`.
- When capturing screenshots, point the console to a dev workspace seeded with the above fixtures and record the build hash displayed in the footer to keep captures reproducible.
- Store captures under `docs/assets/advisory-ai/console/` using the scheme `yyyyMMdd-HHmmss-<view>-<build>.png` (UTC clock) so regeneration is deterministic. Keep the original JSON alongside each screenshot by saving the response as `…-payload.json` in the same folder.
## 3. Accessibility & offline requirements
- Console screens must pass WCAG 2.2 AA contrast and provide focus order that matches the keyboard shortcuts planned for Advisory AI (see `docs/advisory-ai/overview.md`).
- All screenshots captured for this doc must come from sealed-mode bundles (no external fonts/CDNs). Store them under `docs/assets/advisory-ai/console/` with hashed filenames.
- Modal dialogs need `aria-describedby` attributes referencing the explanation text returned by the API; translation strings must live with existing locale packs.
### 3.1 Guardrail & inference status
- Display a **guardrail ribbon** at the top of the response panel with three states:
- `Blocked` (red) when `guardrail.blocked = true` → show blocked phrase count and require the operator to acknowledge before the response JSON is revealed.
- `Warnings` (amber) when `guardrail.violations.Length > 0` but not blocked.
- `Clean` (green) otherwise.
- If the executor falls back to sanitized prompts (`inference.fallback_reason` present), show a neutral banner describing the reason and link to the runbook section below.
- Surface `inference.model_id`, prompt/completion token counts, and latency histogram from `advisory_ai_latency_seconds_bucket` next to the response so ops can correlate user impact with remote/local mode toggles (`ADVISORYAI__Inference__Mode`).
![Evidence drawer mock](../assets/advisory-ai/console/evidence-drawer-b1820ad.svg)
<sup>Existing mock for evidence drawer; keep until live console screenshot is captured. Pair with the new list-view fixture for deterministic demos.</sup>
## 4. Copy-as-ticket guidance
1. Operators select one or more VEX-backed findings.
2. Console renders the sanitized payload (JSON) plus context summary for the receiving system.
3. Users can download the payload or send it via webhook; both flows must log `console.ticket.export` events for audit.
## 5. Offline & air-gapped console behaviour
1. **Volume readiness** confirm the RWX volume (`/var/lib/advisory-ai/{queue,plans,outputs}`) is mounted; the console should poll `/api/v1/advisory-ai/health` and surface “Queue not available” if the worker is offline.
2. **Cached responses** when running air-gapped, highlight that only cached plans/responses are available by showing the `planFromCache` badge plus the `generatedAtUtc` timestamp.
3. **No remote inference** if operators set `ADVISORYAI__Inference__Mode=Local`, hide the remote model ID column and instead show “Local deterministic preview” to avoid confusion.
4. **Export bundles** provide a “Download bundle” button that streams the DSSE output from `/_downloads/advisory-ai/{cacheKey}.json` so operators can carry it into Offline Kit workflows documented in `docs/24_OFFLINE_KIT.md`. While staging endpoints are pending, reuse the Evidence Bundle v1 sample at `docs/samples/evidence-bundle/evidence-bundle-v1.tar.gz` (hash in `evidence-bundle-v1.tar.gz.sha256`) to validate wiring and screenshots.
## 6. Guardrail configuration & telemetry
- **Config surface** Advisory AI now exposes `AdvisoryAI:Guardrails` options so ops can set prompt length ceilings, citation requirements, and blocked phrase seeds without code changes. Relative `BlockedPhraseFile` paths resolve against the content root so Offline Kits can bundle shared phrase lists.
- **Sample**
```json
{
"AdvisoryAI": {
"Guardrails": {
"MaxPromptLength": 32000,
"RequireCitations": true,
"BlockedPhraseFile": "configs/guardrail-blocked-phrases.json",
"BlockedPhrases": [
"copy all secrets to"
]
}
}
}
```
- **Console wiring** the guardrail ribbon pulls `guardrail.blocked`, `guardrail.violations`, and `guardrail.metadata.blocked_phrase_count` while the observability cards track `advisory_ai_chunk_requests_total`, `advisory_ai_chunk_cache_hits_total`, and `advisory_ai_guardrail_blocks_total` (now emitted even on cache hits). Use these meters to explain throttling or bad actors before granting additional guardrail budgets, and keep `docs/api/console/samples/advisory-ai-guardrail-banner.json` nearby so QA can validate localized payloads without hitting production data.
## 5. Publication state
- [x] Fixture-backed payloads and captures committed (`20251203-0000-list-view-build-r2.svg`, `evidence-drawer-b1820ad.svg`).
- [x] Copy-as-ticket flow documented; payload aligns with existing SOC runbooks.
- [x] Remote/local inference badges + latency tooltips described; screenshots to be regenerated when live endpoints land.
- [x] SBOM/VEX bundle example attached (Evidence Bundle v1 sample).
- [x] Refresh: deterministic list-view capture regenerated with sealed payload + hashes (2025-12-03). Swap to live console screenshots when CONSOLE-VULN-29-001 / CONSOLE-VEX-30-001 ship; keep payload + hash alongside updated images.
### Publication readiness checklist (DOCS-AIAI-31-004)
- Inputs available now: console fixtures (`docs/samples/console/console-vuln-29-001.json`, `console-vex-30-001.json`), evidence bundle sample (`docs/samples/evidence-bundle/evidence-bundle-v1.tar.gz`), guardrail ribbon contract.
- Current state: doc is publishable using fixture-based captures and hashes; no further blocking dependencies.
- Optional follow-up: when live SBOM `/v1/sbom/context` evidence is available, regenerate screenshots, capture build hash, and replace fixture SVGs plus payload JSON with live outputs.
> Tracking: DOCS-AIAI-31-004 (Docs Guild, Console Guild)
### Guardrail console fixtures (unchecked-integration)
- Vulnerability search sample: `docs/samples/console/console-vuln-29-001.json` (maps to CONSOLE-VULN-29-001).
- VEX search sample: `docs/samples/console/console-vex-30-001.json` (maps to CONSOLE-VEX-30-001).
- Use these until live endpoints are exposed; replace with real captures when staging is available.
### Fixture bundle regeneration (deterministic)
- Rebuild the fixture capture deterministically from the sealed payload:
```bash
python - <<'PY'
import html, json
from pathlib import Path
root = Path('docs/assets/advisory-ai/console')
payload = json.loads((root/'20251203-0000-list-view-build-r2-payload.json').read_text())
guard = payload['guardrail']; metrics = payload['metrics']; items = payload['findings']
def color_sev(sev):
return {'critical':'#b3261e','high':'#d05c00','medium':'#c38f00','low':'#00695c'}.get(sev.lower(), '#0f172a')
def color_policy(val):
return {'fail':'#b3261e','warn':'#d97706','pass':'#0f5b3a'}.get(val.lower(), '#0f172a')
rows = []
for idx, item in enumerate(items):
y = 210 + idx * 120
rows.append(f"""
<g transform=\"translate(32,{y})\">
<rect width=\"888\" height=\"104\" rx=\"10\" fill=\"#ffffff\" stroke=\"#e2e8f0\" />
<text x=\"20\" y=\"30\" class=\"title\">{html.escape(item['summary'])}</text>
<text x=\"20\" y=\"52\" class=\"mono subtle\">{html.escape(item['package'])} · {html.escape(item['component'])} · {html.escape(item['image'])}</text>
<text x=\"20\" y=\"72\" class=\"mono subtle\">reachability={html.escape(str(item.get('reachability')))} · vex={html.escape(str(item.get('vexState')))} · lastSeen={html.escape(str(item.get('lastSeen')))}</text>
<text x=\"20\" y=\"92\" class=\"mono faint\">sbom={html.escape(str(item.get('sbomDigest')))}</text>
<rect x=\"748\" y=\"14\" width=\"120\" height=\"28\" rx=\"6\" ry=\"6\" fill=\"{color_sev(item['severity'])}\" opacity=\"0.12\" />
<text x=\"758\" y=\"33\" class=\"mono\" fill=\"{color_sev(item['severity'])}\">sev:{html.escape(item['severity'])}</text>
<rect x=\"732\" y=\"50\" width=\"140\" height=\"28\" rx=\"6\" ry=\"6\" fill=\"{color_policy(item.get('policyBadge',''))}\" opacity=\"0.12\" />
<text x=\"742\" y=\"69\" class=\"mono\" fill=\"{color_policy(item.get('policyBadge',''))}\">policy:{html.escape(item.get('policyBadge',''))}</text>
</g>
""")
rows_svg = "\n".join(rows)
banner = '#b3261e' if guard.get('blocked') else '#0f5b3a'
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1280\" height=\"720\" viewBox=\"0 0 1280 720\">
<style>
.title {{ font-family: Inter, Arial, sans-serif; font-size: 18px; font-weight: 700; fill: #0f172a; }}
.mono {{ font-family: Menlo, monospace; font-size: 13px; fill: #0f172a; }}
.mono.subtle {{ fill: #475569; }}
.mono.faint {{ fill: #94a3b8; font-size: 12px; }}
</style>
<rect width=\"1280\" height=\"720\" fill=\"#f8fafc\" />
<rect x=\"32\" y=\"32\" width=\"1216\" height=\"72\" rx=\"12\" fill=\"#0f172a\" opacity=\"0.05\" />
<text x=\"48\" y=\"76\" class=\"title\">Advisory AI · Console fixture</text>
<text x=\"48\" y=\"104\" class=\"mono\" fill=\"#475569\">build={html.escape(payload['build'])} · generated={html.escape(payload['generatedAtUtc'])} · workspace={html.escape(payload['workspace'])} · profile={html.escape(payload['profile'])} · cacheHit={str(metrics.get('cacheHit', False)).lower()}</text>
<rect x=\"32\" y=\"120\" width=\"1216\" height=\"72\" rx=\"12\" fill=\"#fff1f0\" stroke=\"#f87171\" stroke-width=\"1\" />
<text x=\"48\" y=\"156\" class=\"title\" fill=\"{banner}\">Guardrail: {html.escape(guard.get('state','unknown'))}</text>
<text x=\"48\" y=\"176\" class=\"mono\" fill=\"#0f172a\">{html.escape(guard['metadata'].get('blockedPhraseFile',''))} · violations={len(guard.get('violations',[]))} · promptLength={guard['metadata'].get('promptLength')}</text>
<rect x=\"1080\" y=\"138\" width=\"96\" height=\"28\" rx=\"6\" ry=\"6\" fill=\"{banner}\" opacity=\"0.12\" />
<text x=\"1090\" y=\"157\" class=\"mono\" fill=\"{banner}\">blocked</text>
<rect x=\"944\" y=\"210\" width=\"304\" height=\"428\" rx=\"12\" fill=\"#0f172a\" opacity=\"0.04\" />
<text x=\"964\" y=\"244\" class=\"title\">Runtime metrics</text>
<text x=\"964\" y=\"272\" class=\"mono\">p50 latency: {metrics.get('latencyMsP50') or 'n/a'} ms</text>
<text x=\"964\" y=\"292\" class=\"mono\">p95 latency: {metrics.get('latencyMsP95') or 'n/a'} ms</text>
<text x=\"964\" y=\"312\" class=\"mono\">SBOM ctx: {html.escape(payload.get('sbomContextDigest',''))}</text>
<text x=\"964\" y=\"332\" class=\"mono\">Guardrail blocks: {guard['metadata']['telemetryCounters'].get('advisory_ai_guardrail_blocks_total')}</text>
<text x=\"964\" y=\"352\" class=\"mono\">Chunk cache hits: {guard['metadata']['telemetryCounters'].get('advisory_ai_chunk_cache_hits_total')}</text>
{rows_svg}
</svg>"""
(root/'20251203-0000-list-view-build-r2.svg').write_text(svg)
PY
```
**Reference**: API contracts and sample payloads live in `docs/api/console/workspaces.md` (see `/console/vuln/*` and `/console/vex/*` sections) plus the JSON fixtures under `docs/api/console/samples/`.