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
VEX decisionsGET /api/findings/v1/vex-decisionsFindings owns the tenant’s recorded decisions; an accepted write alone does not prove gate application.
Evidence drawerGET /api/vulnerabilities/v1/issues/{vulnerabilityId}/{productKey}/facts and the fact evidence routeRetain issuer provenance; Policy’s effective-status API supplies the tenant outcome.
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.ndjson37e56d1b89beef222b7402877ad5013fdaccdd08adde6d048b34d775bb1cb87cSSE stream sample.
docs/api/console/samples/vuln-findings-sample.json51b6e505a6d2d25ff8d86d25cce73fc73ebedf5bd2255b919cfc733f858f35d8Findings 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.svg72de97bc1001ea20509641694e510c348fd075bdb964167d5d7f62b05d76c39fEvidence drawer mock (keep until live capture).
docs/modules/platform/samples/console-vex-30-001.jsoned14b03435784ea225d542d59cf1795d537585124d90abb3432d0cbdc0256da4Console VEX search fixture.
docs/modules/platform/samples/console-vuln-29-001.jsonb4bb51583c27a574db7ab7be967f5cd14fc7ce4f328b0b865bbbd81a4809f09fConsole 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. Readiness boundary (PCD-456, ad9edbcef5, 2026-10-08) – the former namespaced health request is unavailable. The host maps /health/live, /health/ready and /health/model; those measure their declared host/model checks, not a worker’s RWX mount or queue progress. A successful host response cannot label the queue ready. Verify mounted storage and a real worker event separately before making that operational claim.
  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.

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