Advisory AI Console Workflows

Audience: Console engineers, documentation authors, and QA verifying the Advisory AI experience in the Stella Ops Console.

This guide is the shared reference for the Advisory AI Console experience — navigation, evidence surfacing, the guardrail ribbon, and offline behaviour. Where live endpoints are not yet wired, it points at the deterministic fixtures used to build and verify the screens, so Console, Docs, and QA work from one source of truth.

Last updated: 2025-12-04 (UTC).

1. Entry points & navigation

2. Evidence surfacing

WorkflowRequired APINotes
Findings overviewGET /console/vuln/findingsMust include policy verdict badge, VEX justification summary, and last-seen timestamps.
Evidence drawerGET /console/vex/statements/{id}Stream SSE chunk descriptions so long-form provenance renders progressively.
Copy as ticketPOST /console/vuln/ticketsReturns signed payload + attachment list for JIRA/ServiceNow templates.

2.1 Plan composer vs response panel

Deterministic fixture snapshot (command output, replaces inline screenshot):

python - <<'PY'
import json, pathlib
payload_path = pathlib.Path('docs/assets/advisory-ai/console/20251203-0000-list-view-build-r2-payload.json')
data = json.loads(payload_path.read_text())
metrics = data.get('metrics', {})
guard = data.get('guardrail', {})
violations = guard.get('violations', [])
print(f"# Advisory AI list view fixture (build {data.get('build')})")
print(f"- workspace: {data.get('workspace')} | generated: {data.get('generatedAtUtc')} | profile: {data.get('profile')} | cacheHit: {str(metrics.get('cacheHit', False)).lower()}")
meta = guard.get('metadata', {})
print(f"- guardrail: state={guard.get('state')} blocked={str(guard.get('blocked', False)).lower()} violations={len(violations)} promptLength={meta.get('promptLength')} blockedPhraseFile={meta.get('blockedPhraseFile')}")
print("\n| severity | policy | summary | reachability | vex | lastSeen | sbom |")
print("| --- | --- | --- | --- | --- | --- | --- |")
for item in data.get('findings', []):
    print("| {severity} | {policy} | {summary} | {reach} | {vex} | {last_seen} | {sbom} |".format(
        severity=item.get('severity'),
        policy=item.get('policyBadge'),
        summary=item.get('summary').replace('|', '\\|'),
        reach=item.get('reachability'),
        vex=item.get('vexState'),
        last_seen=item.get('lastSeen'),
        sbom=item.get('sbomDigest'),
    ))
PY
# Advisory AI list view fixture (build console-fixture-r2)
- workspace: tenant-default | generated: 2025-12-03T00:00:00Z | profile: standard | cacheHit: true
- guardrail: state=blocked_phrases blocked=true violations=1 promptLength=12488 blockedPhraseFile=configs/guardrails/blocked-phrases.json

| severity | policy | summary | reachability | vex | lastSeen | sbom |
| --- | --- | --- | --- | --- | --- | --- |
| high | fail | jsonwebtoken <10.0.0 allows algorithm downgrade. | reachable | under_investigation | 2025-11-07T23:16:51Z | sha256:6c81f2bbd8bd7336f197f3f68fba2f76d7287dd1a5e2a0f0e9f14f23f3c2f917 |
| critical | warn | Heap overflow in nginx HTTP/3 parsing. | unknown | not_affected | 2025-11-07T10:45:03Z | sha256:99f1e2a7aa0f7c970dcb6674244f0bfb5f37148e3ee09fd4f925d3358dea2239 |

2.2 Guardrail ribbon payloads

2.3 SBOM / DSSE evidence hooks

Fixture hashes (run from repo root)

Fixturesha256Notes
docs/api/console/samples/advisory-ai-guardrail-banner.jsonbd85eb2ab4528825c17cd0549b547c2d1a6a5e8ee697a6b4615119245665cc02Guardrail ribbon projection.
docs/api/console/samples/vex-statement-sse.ndjson57d7bf9ab226b561e19b3e23e3c8d6c88a3a1252c1ea471ef03bf7a237de8079SSE stream sample.
docs/api/console/samples/vuln-findings-sample.jsonaf3459e8cf7179c264d1ac1f82a968e26e273e7e45cd103c8966d0dd261c3029Findings overview payload.
docs/assets/advisory-ai/console/20251203-0000-list-view-build-r2-payload.json336c55d72abea77bf4557f1e3dcaa4ab8366d79008670d87020f900dcfc833c0List-view sealed payload.
docs/assets/advisory-ai/console/20251203-0000-list-view-build-r2.svgc55217e8526700c2d303677a66351a706007381219adab99773d4728cc61f293Deterministic list-view capture.
docs/assets/advisory-ai/console/evidence-drawer-b1820ad.svg9bc89861ba873c7f470c5a30c97fb2cd089d6af23b085fba2095e88f8d1f8edeEvidence drawer mock (keep until live capture).
docs/modules/platform/samples/console-vex-30-001.jsonf6093257134f38033abb88c940d36f7985b48f4f79870d5b6310d70de5a586f9Console VEX search fixture.
docs/modules/platform/samples/console-vuln-29-001.json921bcb360454e801bb006a3df17f62e1fcfecaaccda471ae66f167147539ad1eConsole vuln search fixture.

3. Accessibility & offline requirements

3.1 Guardrail & inference status

Guardrail ribbon projection (command output, replaces mock screenshot):

python - <<'PY'
import json, pathlib
p = pathlib.Path('docs/api/console/samples/advisory-ai-guardrail-banner.json')
obj = json.loads(p.read_text())
guard = obj['guardrail']
meta = guard['metadata']
print('# Guardrail ribbon projection (banner sample)')
print(f"- blocked: {guard['blocked']} | state: {guard['state']} | violations: {len(guard['violations'])}")
print(f"- planFromCache: {meta.get('planFromCache')} | blockedPhraseFile: {meta.get('blockedPhraseFile')} | promptLength: {meta.get('promptLength')}")
print('- telemetry counters: ' + ', '.join(f"{k}={v}" for k,v in meta['telemetryCounters'].items()))
print('- links: plan={plan} | chunks={chunks} | logs={logs}'.format(
    plan=meta['links'].get('plan'),
    chunks=meta['links'].get('chunks'),
    logs=meta['links'].get('logs'),
))
print('\nViolations:')
for idx, v in enumerate(guard['violations'], 1):
    print(f"{idx}. {v['kind']} · phrase='{v['phrase']}' · weight={v.get('weight')}")
PY
# Guardrail ribbon projection (banner sample)
- blocked: True | state: blocked_phrases | violations: 1
- planFromCache: True | blockedPhraseFile: configs/guardrails/blocked-phrases.json | promptLength: 12488
- telemetry counters: advisory_ai_guardrail_blocks_total=17, advisory_ai_chunk_cache_hits_total=42
- 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

Violations:
1. blocked_phrase · phrase='copy all secrets to external bucket' · weight=0.92

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 the Offline Kit workflows documented in OFFLINE_KIT.md. While staging endpoints are pending, reuse the Evidence Bundle v1 sample at docs/modules/evidence-locker/samples/evidence-bundle-v1.tar.gz (hash in evidence-bundle-v1.tar.gz.sha256) to validate wiring and any optional visual captures.

6. Guardrail configuration & telemetry

7. Publication state

Publication readiness checklist (DOCS-AIAI-31-004)

Tracking: DOCS-AIAI-31-004 (Docs Guild, Console Guild)

Guardrail console fixtures (unchecked-integration)

Fixture bundle regeneration (deterministic)

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
sha256sum docs/assets/advisory-ai/console/20251203-0000-list-view-build-r2.{svg,payload.json}
# expected:
# c55217e8526700c2d303677a66351a706007381219adab99773d4728cc61f293  ...-build-r2.svg
# 336c55d72abea77bf4557f1e3dcaa4ab8366d79008670d87020f900dcfc833c0  ...-build-r2-payload.json

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/.