Advisory AI architecture
Captures the retrieval, guardrail, and inference packaging requirements defined in the Advisory AI implementation plan and related module guides. Configuration knobs (inference modes, guardrails, cache/queue budgets) now live in
docs/modules/policy/guides/assistant-parameters.mdper DOCS-AIAI-31-006.
Tenant identity (claim-bound). The module ships two hosts with different inbound auth, and both take the data-isolation tenant from the authenticated
stellaops:tenantclaim. A caller-supplied tenant is never an isolation key — it is the cross-tenant vector, not a safety property.
advisory-ai-web(StellaOps.AdvisoryAI.WebService) — envelope-only. Identity resolves exclusively from the gateway-signed identity envelope;InboundIdentityHeaderStripMiddlewarestripsX-Tenant-Id-style headers at ingress beforeUseAuthentication(), and the legacyAdvisoryAiHeaderAuthenticationHandlerhas been deleted (see §7.5).opsmemory-web(StellaOps.OpsMemory.WebService) — resource-server bearer + claim-bound tenancy.AddStellaOpsResourceServerAuthentication+UseStellaOpsTenantMiddleware; the/api/v1/opsmemorygroup carries.RequireTenant(), and every handler isolates on the claim resolved byOpsMemoryTenantResolver. ThetenantIdquery parameter and theRecordDecisionRequest.TenantIdbody field are optional conflict checks only: disagreeing with the claim is400 tenant_conflict, and a request with no tenant claim is400 tenant_missing(see §15, “Tenancy contract”).Landed in Sprint
SPRINT_20260712_001(TEN-1). Before it, OpsMemory endpoints passed the caller’stenantIdstraight toPostgresOpsMemoryStoreas the sole isolation key and never consulted the claim — on the compose bypass networks that was an unauthenticated cross-tenant read/write. The library-level guard inPlaybook/PlaybookSuggestionService(throws when handed a blank tenant; the legacy shared-"default"fallback is gone) is defence-in-depth for internal callers of the library — the value the WebService hands it is always the claim tenant.
1) Goals
- Summarise advisories/VEX evidence into operator-ready briefs with citations.
- Explain conflicting statements with provenance and trust weights (using VEX Lens & Excititor data).
- Suggest remediation plans aligned with Offline Kit deployment models and scheduler follow-ups.
- Operate deterministically where possible; cache generated artefacts with digests for audit.
2) Pipeline overview
+---------------------+
Concelier/VEX Lens | Evidence Retriever |
Policy Engine ----> | (vector + keyword) | ---> Context Pack (JSON)
Zastava runtime +---------------------+
|
v
+-------------+
| Prompt |
| Assembler |
+-------------+
|
v
+-------------+
| Guarded LLM |
| (local/host)|
+-------------+
|
v
+-----------------+
| Citation & |
| Validation |
+-----------------+
|
v
+----------------+
| Output cache |
| (hash, bundle) |
+----------------+
3) Retrieval & context
Hybrid search: vector embeddings (SBERT-compatible) + keyword filters for advisory IDs, PURLs, CVEs.
Context packs include:
- Advisory raw excerpts with highlighted sections and source URLs.
- VEX statements (normalized tuples + trust metadata).
- Policy explain traces for the affected finding.
- Runtime/impact hints from Zastava (exposure, entrypoints).
- Export-ready remediation data (fixed versions, patches).
SBOM context retriever (AIAI-31-002) hydrates:
- Version timelines (first/last observed, status, fix availability).
- Dependency paths (runtime vs build/test, deduped by coordinate chain).
- Tenant environment flags (prod/stage toggles) with optional blast radius summary.
- Service-side clamps: max 500 timeline entries, 200 dependency paths, with client-provided toggles for env/blast data.
AddSbomContext(...)(SbomContextServiceCollectionExtensions) registers the typedISbomContextClientHTTP client (SbomContextHttpClient) that calls the SBOM service.ISbomContextRetriever.RetrieveAsyncis the in-process retriever entry point that wraps it. ANullSbomContextClientexists for harness/no-op scenarios.
Sample configuration (wire the real SBOM base URL via
SbomContextClientOptions):services.AddSbomContext(options => { options.BaseAddress = new Uri("https://sbom-service.internal"); options.ContextEndpoint = "api/sbom/context"; // default options.Tenant = configuration["TENANT_ID"]; options.TenantHeaderName = "X-StellaOps-TenantId"; // default });Note:
SbomContextClientOptionsexposes onlyBaseAddress,ContextEndpoint,Tenant, andTenantHeaderName. It has no API-key / user-agent fields; outbound auth to internal services is supplied by the Authorityclient_credentialsbearer (see §7.6). In hosted deployments the SBOM base address is normally set throughAdvisoryAI:SbomBaseAddressandAddAdvisoryAiCorerather than a manualAddSbomContextcall.After configuration, issue a smoke request (
ISbomContextRetriever.RetrieveAsync) during deployment validation to confirm end-to-end connectivity before enabling Advisory AI endpoints.
Retriever requests and results are trimmed/normalized before hashing; metadata (counts, provenance keys) is returned for downstream guardrails. Unit coverage ensures deterministic ordering and flag handling.
All context references include content_hash and source_id enabling verifiable citations.
4) Guardrails
- Prompt templates enforce structure: summary, conflicts, remediation, references.
- Response validator ensures:
- No hallucinated advisories (every fact must map to input context).
- Chat citations use stable
CIT-##-<hash>identifiers that reference actual source snippets and content hashes. - Remediation suggestions only cite policy-approved sources (fixed versions, vendor hotfixes).
- Moderation/PII filters prevent leaking secrets; responses failing validation are rejected and logged. Chat inference rejects invalid JSON, unknown citation IDs, and factual claims without citations.
- Pre-flight guardrails redact secrets (AWS keys, generic API tokens, PEM blobs), block “ignore previous instructions”-style prompt injection attempts, enforce citation presence, and cap prompt payload length (default 16 kB). Guardrail outcomes and validation failures surface via the
advisory_ai_guardrail_blocks_totalandadvisory_ai_validation_failures_totalcounters (withadvisory_outputs_storedrecording aguardrail_blockedflag); all carry atask_typedimension. See §9 for the full meter inventory.
5) Deterministic tooling
- Version comparators — offline semantic version + RPM EVR parsers with range evaluators. Supports chained constraints (
>=,<=,!=) used by remediation advice and blast radius calcs.- Registered via
AddAdvisoryDeterministicToolsetfor reuse across orchestrator, CLI, and services.
- Registered via
- Orchestration pipeline — see
orchestration-pipeline.mdfor prerequisites, task breakdown, and cross-guild responsibilities before wiring the execution flows. - Planned extensions — NEVRA/EVR comparators, ecosystem-specific normalisers, dependency chain scorers (AIAI-31-003 scope).
- Exposed via internal interfaces to allow jobengine/toolchain reuse; all helpers stay side-effect free and deterministic for golden testing.
6) Output persistence
- Generated outputs are persisted by the file-backed
FileSystemAdvisoryOutputStore(the defaultIAdvisoryOutputStore), partitioned by cache key / task type / profile under the outputs data root — there is noadvisory_ai_outputsSQL table. Each artefact carries:output_hash(sha256 of JSON response).input_digest(hash of context pack).summary,conflicts,remediation,citations.generated_at,model_id,profile(free-form label; see §7).signatures(optional DSSE if run in deterministic mode).
- Offline bundle format contains
summary.md,citations.json,context_manifest.json,signatures/. - PostgreSQL
advisoryaischema (auto-migrated). The schema is owned by AdvisoryAI and auto-migrated on startup from embedded SQL insrc/AdvisoryAI/StellaOps.AdvisoryAI/Storage/Migrations/(viaAddStartupMigrations), not by external init scripts. There is exactly one live migration file,001_v1_advisoryai_baseline.sql— the pre-1.0001–010chain was collapsed into it and now sits underStorage/Migrations/_archived/pre_1.0/mig061/, excluded from embedding (.csproj:16). Do not grep for009_ai_runtime_state.sqlet al.; grep the baseline. The runtime-state tables are:- Consent & attestation:
advisoryai.ai_consents(baseline line 670),advisoryai.ai_run_attestations,advisoryai.ai_claim_attestations(folded in from the pre-1.0009). - Runtime replay/ledger/chat:
advisoryai.runtime_explanations(line 735),advisoryai.runtime_policy_intents,advisoryai.runtime_runs,advisoryai.runtime_chat_settings_overrides,advisoryai.conversations(line 805),advisoryai.turns(line 822) (from the pre-1.0010). - Chat audit:
advisoryai.chat_sessions(line 27),chat_messages,chat_policy_decisions,chat_tool_invocations,chat_evidence_links(from the pre-1.0001). - Knowledge/Unified Search + analytics/feedback:
advisoryai.kb_doc(line 135),kb_chunk,api_spec,api_operation,doctor_search_projection,entity_alias,search_events(line 416),search_history,search_feedback(line 445),search_quality_alerts(line 495) (from the pre-1.0002–008), with FTS/trigram indexing (English + multilingual).
- Consent & attestation:
- Runtime storage connection resolution is
AdvisoryAI:Storage:ConnectionString->ConnectionStrings:Default->Database:ConnectionString; when no durable store is configured the host fails fast instead of silently falling back to process-local consent, explanation, policy, run, conversation, chat-settings, signer, retrieval, or data-provider bindings. - Process-local
Null*,NoOp*, andInMemory*runtime bindings are allowed only for explicit local harnesses: environmentDevelopmentorTestingplusAdvisoryAI:RuntimeBindings:AllowLocalHarnessFakes=true. - Advisory AI attestation signing is explicit:
IAiAttestationEnvelopeSignermust be registered beforeCreate*AttestationAsync(..., sign: true)can produce a DSSE envelope.RunService.AttestAsyncalso requires a configuredIRunAttestationSigneror registeredIAiAttestationEnvelopeSigner; missing signing configuration, empty signatures, and known placeholder markers fail closed instead of creating mock signatures. - Evidence pack creation never synthesizes
sha256:unknown. Each evidence item must provide a validsha256:<64 hex>digest, or the API computes one from normalized snapshot data when snapshot data is present and deterministic. Empty snapshot data without a caller digest is rejected.
7) Profiles & sovereignty
- Profile is a free-form label, not an enumerated catalog (current code).
AdvisoryTaskRequest.Profileis astringthat defaults to"default"(src/AdvisoryAI/StellaOps.AdvisoryAI/Orchestration/AdvisoryTaskRequest.cs). It is threaded into the plan cache key (AdvisoryPipelineOrchestratorwritesbuilder["profile"]) and into the output store partition (AdvisoryOutputStorerequires a non-empty profile). The pipeline does not ship a built-in profile registry that mapsfips-local/gost-local/cloud-openaito allowed models, key management, or telemetry endpoints — passing such a value simply produces a separate cache/output partition under that label. - (Draft / roadmap.) Named sovereignty profiles (
fips-local,gost-local) and a disabled-by-default cloud profile that bind specific models, KMS, and telemetry sinks are a forward design goal and are not yet implemented as a profile catalog. Today the model/provider choice is governed byAdvisoryAI:Chat:Inference:Providerand the LLM-provider plugin configuration (see §13), independently of the requestprofile. - CryptoProfile / signing integration: generated artefacts and attestations can be signed via a registered signer (
IAiAttestationEnvelopeSigner/IRunAttestationSigner/IEvidencePackSigner) to satisfy procurement/trust requirements (see §6). Signing is explicit and fails closed when no signer is configured.
7.5) Authentication & authorization (Sprint SPRINT_20260430_002)
Audit finding A2 (
docs-archive/qa/audits/microservice-audit-pass2-2026-04-29.md): the previously registeredAdvisoryAiHeaderAuthenticationHandleraccepted rawX-User-Id,X-Tenant-Id, andX-StellaOps-Scopesheaders as authoritative identity. That handler has been deleted as a security hardening; the current contract is gateway-envelope-only.
- Single auth source. AdvisoryAI’s WebService trusts only the gateway-signed identity envelope (
X-StellaOps-Identity-Envelope+X-StellaOps-Identity-Envelope-Signature, HMAC-SHA256, verified byUseIdentityEnvelopeAuthentication()fromStellaOps.Router.AspNet). The registered authentication scheme (AdvisoryAiEnvelope) is a thin handler that surfaces the envelope-validated principal to ASP.NET authorization; there is no fallback path. - Inbound header strip.
InboundIdentityHeaderStripMiddlewareruns beforeUseAuthentication()and unconditionally removesX-User-Id,X-Tenant-Id,X-StellaOps-Actor,X-StellaOps-Scopes,X-StellaOps-TenantId,X-Stella-Scopes, andX-Scopesfrom every inbound request, so a malicious or misconfigured caller cannot pre-populate identity even if the request reaches the service directly (sidecar, debug port, misrouted gateway). - Behavior in every environment. The strip and the envelope verification are not conditional on
ASPNETCORE_ENVIRONMENT. Requests with raw identity headers and no envelope return HTTP 401 inDevelopment,Testing, andProductionalike. - Claim mapping. Authorization policies (
AdvisoryAIPolicies.View / Operate / Admin) and any handler-internal scope/tenant lookups read fromhttpContext.User.Claims(envelope-derived), not from raw headers. Tenant resolution usesstellaops:tenantthentenantthentenant_id; subject usessubthenClaimTypes.NameIdentifier; scopes usescope(one claim per envelope scope) andscp(alias). - Operator note. Internal callers that previously sent only raw identity headers (CLI, smoke scripts, ad-hoc curl) must route through the gateway to receive a signed envelope; direct calls without an envelope will be rejected with 401.
7.6) Outbound service-to-service auth (Authority client_credentials)
Inbound auth (§7.5) is envelope-only. Outbound calls from AdvisoryAI to other internal services are a separate concern handled by an OAuth client_credentials bearer, configured under AdvisoryAi:Authority and wired in Program.cs:
- When
AdvisoryAi:Authority:Enabled=true, the service registersAddStellaOpsAuthClient(discovery + JWKS + token client) and attaches aStellaOpsBearerTokenHandlerto the named internal HTTP clientsvex-internal,scanner-internal,policy-internal,graph-internal, andtimeline-internal. The UnifiedSearch live adapters (VEX / Policy / Findings / Graph / Timeline) need this bearer because the upstream tenant resolver reads tenant only from a signed JWT claim (stellaops:tenant); without it the upstreams reply400 tenant_missingand the adapters fall back to a stale embedded snapshot. - Config keys:
Enabled,Issuer,MetadataAddress,ClientId,ClientSecret,Scope(defaultadvisory:read findings:read graph:read policy:read vex:read),Tenant(defaultdefault),BackchannelTimeoutSeconds. The matchingstellaops-advisory-ai-internalOAuth client + service account are seeded by the Authority seed baselineS001_v1_authority_operational_baseline.sql(client insert at line 835; folded in from the pre-1.0S005_advisory_ai_authority_client.sql, which is archived and no longer embedded). - The token cache is forced in-memory (AdvisoryAI runs single-replica today).
8) APIs
All HTTP routes are mapped in src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs (plus the per-feature Endpoints/*.cs). The canonical route prefix is /v1/advisory-ai/*; chat lives under /api/v1/chat/* and unified search under /v1/search/*. There is no /api/v1/advisory/* surface — that path is historical and does not exist in code. Every business endpoint is authorized by a named policy (§7.5) and rate-limited by the advisory-ai token bucket (30 req/min per X-StellaOps-Client); write paths are wrapped with .Audited(...).
Authorization policies resolve scopes via
StellaOpsScopes:advisory-ai:view(View),advisory-ai:operate(Operate, implies View),advisory-ai:admin(Admin, implies Operate). The legacyadvisory:run/advisory:explain/advisory:companion/advisory:remediate/advisory:justifystrings still appear in some handler-internalEnsureAuthorizedchecks, but those strings are not registered Authority scopes — modern gateway envelopes carry theadvisory-ai:*scopes that the named policies enforce first.
Pipeline & outputs
POST /v1/advisory-ai/pipeline/{taskType}(Operate, audited) — creates a plan fortaskType(summary|conflict|remediation), caches it, and enqueues execution. Body:{advisoryKey, artifactId?, artifactPurl?, policyVersion?, profile, preferredSections?, forceRefresh}. Returns the plan (cache key, prompt template hash, token budget, guardrail metadata).POST /v1/advisory-ai/pipeline:batch(Operate, audited) — same as above for a batch of plan requests.GET /v1/advisory-ai/outputs/{cacheKey}?taskType=...&profile=...(View) — retrieves a cached output.profiledefaults todefaultwhen omitted (notfips-local);taskTypeis required and parsed case-insensitively.
Explanation & companion
POST /v1/advisory-ai/explain(Operate, audited) — evidence-anchored explanation for a finding/vulnerability.GET /v1/advisory-ai/explain/{explanationId}/replay(View) — deterministic replay of a stored explanation.POST /v1/advisory-ai/companion/explain(Operate) — composes the explanation with deterministic runtime signals from Zastava-compatible observers; request extends explain fields withruntimeSignals[], response returnscompanionId,companionHash, composed summary lines, and normalized runtime highlights.
Remediation (Remedy Autopilot)
POST /v1/advisory-ai/remediation/plan(Operate, audited) andPOST /v1/advisory-ai/remediate(Operate, alias).POST /v1/advisory-ai/remediation/apply(Operate, audited) — dispatches to the SCM-specificIPullRequestGeneratorselected byscmType.GET /v1/advisory-ai/remediation/status/{prId}?scmType=...(View) —scmTypedefaults togithub.
Policy Studio (Copilot)
POST /v1/advisory-ai/policy/studio/parseand.../generate(both Operate) are wired.POST /v1/advisory-ai/policy/studio/validateand.../compile(Operate) currently return HTTP 501 Not Implemented — they are not yet wired to durable generated-rule storage.
Consent, justification, rate limits (VEX-AI)
GET|POST|DELETE /v1/advisory-ai/consent(View/Operate; POST and DELETE audited) — durable consent underadvisoryai.ai_consents.POST /v1/advisory-ai/justify(Operate) — VEX justification draft.GET /v1/advisory-ai/rate-limits(View).
Chat gateway (/api/v1/chat/*, all Operate)
POST /query,POST /query/stream(SSE),POST /intent,POST /evidence-preview.GET|PUT|DELETE /settings,GET /doctor,GET /status.- Legacy
/v1/advisory-ai/conversations*endpoints remain for backward compatibility and are deprecated (sunset 2026-12-31 UTC) in favor of/api/v1/chat/*. - The legacy conversation handlers retain authorization, tenant binding, runtime dispatch, persistence, and HTTP/SSE ownership in the WebService composition root. Their deterministic value projection is isolated in internal
AdvisoryConversationValues: response/evidence/action mapping, query-context enrichment, header/role parsing, citation formatting, confidence/token normalization, and token streaming. This boundary is deliberately behavior-preserving and does not create a second runtime or endpoint surface.
Runs ledger (/v1/advisory-ai/runs, group View; mutations Operate, audited)
POST /(create),GET /{runId},GET /(query),GET /{runId}/timeline,GET /active,GET /pending-approval.- Turn/action/approval lifecycle:
POST /{runId}/events,/turns/user,/turns/assistant,/actions,/actions/{actionEventId}/execute,/approval/request,/approval/decide,/artifacts,/complete,/cancel,/handoff,/attest.
Attestations & evidence packs
GET /v1/advisory-ai/runs/{runId}/attestation,.../claims,GET /v1/advisory-ai/attestations/recent,POST /v1/advisory-ai/attestations/verify(all View).POST /v1/evidence-packs(Operate),GET /v1/evidence-packs[/{packId}],.../export,POST .../sign(Operate),POST .../verify(View),GET /v1/runs/{runId}/evidence-packs(View).
Knowledge Search & Unified Search
POST /v1/advisory-ai/search(Operate),POST /v1/advisory-ai/index/rebuild(Admin).POST /v1/search/query(Operate),.../suggestions/evaluate,.../synthesize(Operate),POST /v1/search/index/rebuild(Admin).- Search analytics/history:
POST|GET|DELETE /v1/advisory-ai/search/analytics|history(View/Operate). Feedback/quality:POST /v1/advisory-ai/search/feedback(View),.../quality/alerts|metrics(Admin).
LLM adapter passthrough (only when AdvisoryAI:Adapters:Llm:Enabled=true)
GET /v1/advisory-ai/adapters/llm/providers(View),POST /v1/advisory-ai/adapters/llm/{providerId}/chat/completions(Operate),POST /v1/advisory-ai/adapters/openai/v1/chat/completions(Operate).- Runtime plugin boundary: the WebService no longer references
StellaOps.AdvisoryAI.Plugin.Unifieddirectly. When the adapter surface is enabled, the host loads the unified LLM adapter from an explicitAdvisoryAI:Adapters:Llm:PluginAssemblyPath,AdvisoryAI:Adapters:Llm:PluginDirectory, or the mounted bundle path/app/plugins/advisoryai/base/llm-unified/. If the bundle is missing or cannot register the realLlmPluginAdapterFactory, completion calls return a controlled unavailable response rather than falling back to in-process implementation code. - Per-provider signed plugins (commit
a36f672403, 2026-06-07, WS1): per the decision to ship per-provider signed assembly plugins (not config-only), AdvisoryAI now loads individualILlmProviderPluginbundles throughMountedLlmProviderRuntimePluginLoaderand merges the survivors intoLlmProviderCatalogso they flow through the existing unifiedLlmPluginAdapterexactly like the built-in providers. Both previously insecure drop-points were hardened to the shared signed admission:AdvisoryAiLlmAdapterPluginBridgereplaced the bareLoadFromAssemblyPathof the mounted unified adapter, and the newAdvisoryAiScmAdapterPluginBridgedoes the same for the identical SCM twin.ListProviderssurfaces rejected bundles (no silent-green). See §11.1 Mounted LLM provider plugin loader for the admission chain, config keys, producer, internal diagnostics, and overlay. A live overlay-up acceptance run is still pending.
Health & infra
GET /health,/health/live,/health/ready(model-gateway-aware),/health/model(detailed gateway payload), plus the build-info endpoint. These are anonymous/unauthenticated.
Pipeline plan/output responses carry output_hash, input_digest, and citations for verification.
9) Observability
- Meter: all instruments use the meter name
StellaOps.AdvisoryAI(defined in bothAdvisoryPipelineMetricsand the hostingAdvisoryAiMetrics). Most carry atask_typedimension; there is noprofilemetric label in current code. - Hosting counters (
StellaOps.AdvisoryAI.Hosting/AdvisoryAiMetrics.cs):advisory_ai_pipeline_requests_total,advisory_ai_pipeline_messages_enqueued_total,advisory_ai_pipeline_messages_processed_total. - Pipeline metrics (
StellaOps.AdvisoryAI/Metrics/AdvisoryPipelineMetrics.cs): countersadvisory_plans_created,advisory_plans_queued,advisory_plans_processed,advisory_outputs_stored,advisory_ai_guardrail_blocks_total,advisory_ai_validation_failures_total; histogramsadvisory_plan_build_duration_seconds,advisory_ai_latency_seconds,advisory_ai_citation_coverage_ratio. - Logs include
output_hash,input_digest,profile,model_id,tenant, and artifact identifiers. Sensitive context is not logged. - Traces:
AdvisoryAiActivitySource.Instanceemits server spans for plan requests, batch plans, explain/replay, companion explain, remediation, policy parse/generate, and justify.
10) Operational controls
- HTTP rate limiting. The WebService registers an ASP.NET token-bucket limiter (
advisory-aipolicy) partitioned by theX-StellaOps-Clientheader: 30 tokens, 30/minute refill, no queue. Requests over budget get HTTP 429. (This is request-level throttling, not orchestrator-/profile-based quota.) - Chat quotas. Chat traffic additionally enforces per-tenant/per-user quotas (requests/min, requests/day, tokens/day, tool-calls/day) and tool allowlists via
IAdvisoryChatQuotaService/AdvisoryChatToolPolicy, layered as global → tenant → user overrides (see §14 and the/api/v1/chat/settings+/doctorendpoints). - Offline/air-gapped deployments run local models packaged with the Offline Kit; model artifacts are validated via manifest digests (operators supply the gateway and weights — Stella Ops does not bundle weights).
11) Hosting surfaces
- WebService (
advisory-ai-web) - exposes the full API surface in §8 (notablyPOST /v1/advisory-ai/pipeline/{taskType}) to materialise plans and enqueue execution messages. - Worker (
advisory-ai-worker) -AdvisoryTaskWorkerhosted service draining the advisory pipeline queue. The queue binding is the file-backedFileSystemAdvisoryTaskQueue(registered byAddAdvisoryAiCore); shared message-transport integration is still pending. The worker also binds the real Concelier-backedIAdvisoryDocumentProvider(AddConcelierAdvisoryDocumentProvider) beforeAddAdvisoryAiCoreso the orchestrator resolves a live document source at boot rather than the fail-closed fallback. - Both hosts register
AddAdvisoryAiCore, which wires the SBOM context client, deterministic toolset, pipeline orchestrator, file-backed queue/plan/output stores, and queue metrics. - QA-only fixture container (
advisory-fixture). An nginx servingdevops/compose/fixtures/integration-fixtures/advisory/data, defined only in theintegration-fixtures/advisory-fixture-offlineoverrides (docker-compose.integration-fixtures.yml) — not in the defaultstella-servicesstack. It exists to feed deterministic advisory inputs to integration tests; correctly gated behind overrides, so it never reaches a production stack. - In supported compose, both hosts depend on
advisory-ai-data-initand use the same initialized/var/lib/advisory-ai/{queue,plans,outputs}named volumes./tmpqueue/cache paths are local-harness only and must not be used by the setup procedure. advisory-ai-webregistersAddAdvisoryAiExplanationRuntimeBindingsbeforeAddAdvisoryAiCore. WhenAdvisoryAI:EvidenceRetrieval:BaseAddressis configured,IEvidenceRetrievalServiceresolves to the Scanner-backed unified evidence adapter rather than the fail-closed fallback. The adapter calls/api/v1/triage/findings/{findingId}/evidencethrough thescanner-internalHTTP client and maps SBOM, reachability, VEX, attestation, manifest, verification, delta, binary-diff, and policy data into deterministic explanation evidence nodes. It also binds the explanation endpoint to the configured AdvisoryAI inference client and a citation extractor that verifies model citations against evidence references.- The compose frontdoor publishes AdvisoryAI through gateway
ReverseProxyroutes (/api/v1/advisory-ai/*,/v1/advisory-ai/*,/api/v1/search/*, and/v1/evidence-packs/*) with preserved gateway auth headers. These paths are not service-discoveryMicroserviceroutes in the supported compose setup becauseadvisory-ai-webis already reachable by container DNS alias and requires the gateway identity envelope. advisory-ai-webregistersAddAdvisoryAiRuntimePersistence, andadvisory-ai-workerregistersAddAdvisoryAiCoreRuntimePersistence; together those paths auto-migrate theadvisoryaischema and bind durable PostgreSQL-backed consent, attestation, explanation replay, policy-intent, run, conversation, and chat-settings stores whenever runtime database configuration is present.- In-memory and null runtime implementations are reserved for explicit local harnesses.
DevelopmentorTestingmay opt in withAdvisoryAI:RuntimeBindings:AllowLocalHarnessFakes=true; every other host, and local hosts without that flag, fail fast when durable/runtime contracts are missing. - SBOM base address + tenant metadata are configured via
AdvisoryAI:SbomBaseAddressand propagated throughAddSbomContext.
11.1) Mounted LLM provider plugin loader (signed bundle admission)
MountedLlmProviderRuntimePluginLoader (src/AdvisoryAI/StellaOps.AdvisoryAI/Inference/LlmProviders/Admission/MountedLlmProviderRuntimePluginLoader.cs, commit a36f672403) discovers, admits, and activates signed per-provider ILlmProviderPlugin bundles from a mounted profile directory and registers the survivors in LlmProviderCatalog so they flow through the existing unified LlmPluginAdapter. Only the AdvisoryAI web service runs the provider loader (the worker does not), and the LLM adapter surface must be enabled (AdvisoryAI:Adapters:Llm:Enabled, default true in the base stack) for mounted providers to be admitted. The loader is fail-closed and de-duplicates by ILlmProviderPlugin.ProviderId (a duplicate is surfaced as rejected, never silently shadowed). A missing/unmounted root is fail-open: AdvisoryAI keeps running with only the built-in providers.
advisory-ai-web also maps the canonical pluginized-compose diagnostics: GET /internal/plugins/status returns the loader’s catalog state, and POST /internal/plugins/probe runs a deterministic catalog probe that marks mounted, discovered, admitted, and loaded provider bundles as responded. The probe does not call an external model endpoint.
Admission chain — each bundle is admitted through the shared SignedRuntimePluginAdmission chokepoint (src/__Libraries/StellaOps.Plugin/Security/SignedRuntimePluginAdmission.cs, promoted from AdvisoryAI in commit c1922c1ce5; AdvisoryAI keeps a thin facade that bakes in module advisoryai, contract runtime-bundle.v1, and capability advisoryai:llm-provider):
- Manifest binding —
idequals the bundle directory name,moduleisadvisoryai,contractVersionisruntime-bundle.v1, the configured profile matches (when non-empty), capabilityadvisoryai:llm-provideris declared, and a well-formed assembly descriptor (relativepath+sha256) is present. - Per-assembly SHA-256 + path-traversal guard — the on-disk assembly bytes must hash to the manifest digest; rooted/escaping paths are rejected; the detached
<assembly>.sigis enforced to the conventional location so a tampered manifest cannot redirect the verifier. - Detached RSA-PKCS1-SHA256 verification —
OfflineDevRsaSha256PluginVerifierwithAllowUnsigned=falseagainst the configured trust root. Only then is theILlmProviderPluginentry type activated viaActivatorUtilities. AnAssemblyLoadContextresolving hook (AdvisoryAiPluginAssemblyResolver) resolves transitiveStellaOps.*deps.
Before code activation, two additional host-level gates keep the mount boundary deterministic. A manifest with enabled=false is reported as disabled with zero providers and no assembly load. When RequireReadOnlyBundles=true (the default), writable bundle directories are reported as rejected with zero providers so the compose read-only mount contract is visible in the probe report.
Configuration (AdvisoryAI:LlmProviders:RuntimePlugins, env prefix ADVISORYAI_):
| Key | Default | Purpose |
|---|---|---|
AdvisoryAI:LlmProviders:RuntimePlugins:RootPath | /app/plugins/advisoryai | Root containing profile directories. |
AdvisoryAI:LlmProviders:RuntimePlugins:Profile | base | Profile; the loader resolves provider bundles under <RootPath>/<Profile>/llm-providers. |
AdvisoryAI:LlmProviders:RuntimePlugins:TrustRootPath | /app/etc/certificates/trust-roots/plugins/advisoryai/cosign.pub | Trust-root public key. |
AdvisoryAI:LlmProviders:RuntimePlugins:RequireReadOnlyBundles | true | Reject writable provider bundle directories; set false only for local diagnostics/tests where temp directories cannot be mounted read-only. |
Hardened drop-points: AdvisoryAiLlmAdapterPluginBridge (mounted unified LLM adapter) and AdvisoryAiScmAdapterPluginBridge (the identical SCM twin) both replaced their bare LoadFromAssemblyPath with the same signed admission, and the /v1/advisory-ai/adapters/llm/... ListProviders surface reports rejected bundles.
Bundle / trust-root layout:
| Purpose | Host path | Container path |
|---|---|---|
| Signed LLM provider bundle | devops/plugins/advisoryai/base/llm-providers/<provider-id>/ (manifest.json + <assembly>.dll + <assembly>.dll.sig) | /app/plugins/advisoryai/base/llm-providers/<provider-id> |
| Operator config/registry | devops/etc/plugins/advisoryai/ | /app/etc/plugins/advisoryai |
| AdvisoryAI plugin trust root | devops/etc/certificates/trust-roots/plugins/advisoryai/cosign.pub | /app/etc/certificates/trust-roots/plugins/advisoryai/cosign.pub |
| Probe scratch | named volume advisoryai-plugin-scratch | /var/lib/stellaops/plugin-scratch/advisoryai |
Bundle producer: devops/build/package-runtime-plugins.ps1 -Module advisoryai -Profile base -UseOfflineDevSigner stages the signed stellaops.advisoryai.llm-provider.ollama bundle under <profile>/llm-providers/. The loader already handles N providers; staging the remote provider + adapter bundles is the documented follow-up. The generated cosign.pub is git-ignored under the advisoryai trust-root directory.
Opt-in compose overlay: devops/compose/docker-compose.plugins.advisoryai.yml layers read-only mounts of devops/plugins/advisoryai/base + the trust root onto advisory-ai-web, keeps the LLM adapter enabled, and restates the loader defaults (ADVISORYAI__AdvisoryAI__LlmProviders__RuntimePlugins__RootPath/Profile/TrustRootPath). Apply with COMPOSE_EXTRA_FILES=docker-compose.plugins.advisoryai.yml ./scripts/compose-cli.ps1 up.
Tests: focused loader admission coverage includes signed load, missing/unmounted root, bad hash, bad signature, duplicate provider ID, unsupported contract, capability mismatch, disabled manifest, writable mount, and malformed/path traversal cases; every reject path admits zero providers. The internal status/probe endpoints have integration coverage for status output, probe response marking, and plugin ID filtering.
Live runtime probe is pending. The loader, hardened bridges, producer, and overlay are committed, and
/internal/plugins/status+/internal/plugins/probeare mapped, but a live overlay-up acceptance probe against a runningadvisory-ai-web(mount the signed Ollama provider, confirmGET /v1/advisory-ai/adapters/llm/providersand/internal/plugins/*report it admitted/responded) has not yet been recorded.
12) QA harness & determinism (Sprint 110 refresh)
- Injection fixtures:
src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/TestData/guardrail-injection-cases.jsonnow enumerates both blocked and allow-listed prompts (redactions, citation checks, prompt-length clamps) while the legacyprompt-injection-fixtures.txtfile continues to supply quick block-only payloads.AdvisoryGuardrailInjectionTestsconsumes both datasets so guardrail regressions surface with metadata (blocked phrase counts, redaction counters, citation enforcement) instead of single-signal failures. - Golden prompts:
summary-prompt.jsonnow pairs withconflict-prompt.json;AdvisoryPromptAssemblerTestsload both to enforce deterministic JSON payloads across task types and verify vector preview truncation (600 characters + ellipsis) keeps prompts under the documented perf ceiling. - Plan determinism:
AdvisoryPipelineOrchestratorTestsshuffle structured/vector/SBOM inputs and assert cache keys + metadata remain stable, proving that seeded plan caches stay deterministic even when retrievers emit out-of-order results. - Execution telemetry:
AdvisoryPipelineExecutorTestsexercise partial citation coverage (target ≥0.5 when only half the structured chunks are cited) soadvisory_ai_citation_coverage_ratioreflects real guardrail quality. - Plan cache stability:
AdvisoryPlanCacheTestsnow seed the in-memory cache with a fake time provider to confirm TTL refresh when plans are replaced, guaranteeing reproducible eviction under air-gapped runs.
13) Deployment profiles, scaling, and local model inference
- Local inference containers.
advisory-ai-webexposes the API/plan cache endpoints whileadvisory-ai-workerdrains the queue and executes prompts. Both containers mount the same shared data root that hosts three deterministic paths:/var/lib/advisory-ai/queue,/var/lib/advisory-ai/plans,/var/lib/advisory-ai/outputs. Compose bundles create named volumes (advisory-ai-{queue,plans,outputs}); Kubernetes/Helm PVC packaging is not a supported Stella Ops release target. - In-house OpenAI-compatible gateway. Production-like chat question answering binds to
AdvisoryAI:Chat:Inference:Provider=inhouse-ternaryoropenai-compatibleand requires an operator-configuredAdvisoryAI:Chat:Inference:BaseUrl. The gateway callsGET /health,GET /v1/models, andPOST /v1/chat/completions; model registry entries must advertise the configured model, optional digest, and required production capabilities (chat.completions,response_format.json_object,citations.required,seed,top_k). Unavailable health/model checks, missing capabilities, digest mismatches, invalid JSON envelopes, and uncited model output return controlled runtime unavailability with structured diagnostics instead of prompt echo or placeholder answers. - Default ternary model pin. Setup guidance (
llm-setup-guide.md) defaults tomicrosoft/bitnet-b1.58-2B-4T, an MIT-licensed native 1.58-bit/ternary roughly 2B-parameter model. The shippedappsettings.jsoncarries the placeholderAdvisoryAI:Chat:Inference:Model=local-32b-ternaryand an emptyBaseUrl/ModelDigest, so operators must overrideModel/BaseUrlto the approved local gateway artifact. Stella Ops does not bundle model weights; operators provide the gateway, model artifact, and expected digest or signed manifest. A future custom/larger Stella Ops model requires release guide and model-selection updates before it becomes the documented default. - Operational health.
advisory-ai-webexposes/health/livefor process liveness,/health/readyfor model-gateway-aware readiness, and/health/modelfor the detailed gateway status payload used by operators and diagnostics. - Deterministic generation contract. Chat requests use
temperature=0,top_p=1,top_k=1(SendTopK=true), a fixedseed(default42), boundedmax_tokens(default512), andresponse_format={type:json_object}— see theAdvisoryAI:Chat:Inferenceblock inappsettings.json. Audit metadata recordsmodel_id, optionalmodel_digest,prompt_template_hash, andcontext_digest. - No inference fallback. Production-like AdvisoryAI chat and provider-based inference do not fall back to dummy providers, null clients, sanitized prompts, prompt previews, empty-output prompt echoes, or cloud models. Local harness fakes require
DevelopmentorTestingplusAdvisoryAI:RuntimeBindings:AllowLocalHarnessFakes=true; otherwise missing configuration is a fail-closed runtime error. Prompt-preview output is labeled withdev-harness.prompt-preview,inference.harness=local-dev-test-only, andinference.production_eligible=false. - Scalability. Start with 1 web replica + 1 worker for up to ~10 requests/minute. For higher throughput, scale
advisory-ai-workerhorizontally; each worker is CPU-bound (2 vCPU / 4 GiB RAM recommended) while the web front end is I/O-bound (1 vCPU / 1 GiB). Because the queue/plan/output stores are content-addressed files, ensure the shared volume delivers ≥500 IOPS and <5 ms latency; otherwise queue depth will lag. - Offline & air-gapped stance. Supported compose/service-manager release manifests avoid external network calls by default and the Offline Kit now publishes the
advisory-ai-webandadvisory-ai-workerimages alongside their SBOMs/provenance. Operators can rehydrate the shared data root from the kit to pre-prime cache directories before enabling the service. - Local runtime truthfulness. The in-process
LlamaCppRuntimedoes not provide inference without a wired native llama.cpp binding. Loading a GGUF file verifies path presence only and then fails closed; digest verification remains available, but no placeholder text or stream chunks are emitted. Use a realILlmProvidersuch as a configured llama.cpp server/Ollama provider for local inference untilAIAI-TRUTH-003-01is replaced by a native runtime implementation. - Provider abstraction & on-prem posture. Two distinct abstractions exist: (1) the chat inference client selected by
AdvisoryAI:Chat:Inference:Provider(inhouse-ternary/openai-compatibleis the default;local,ollama,openai,claudeclients also exist underChat/Inference/); and (2) theILlmProviderplugin catalog (Inference/LlmProviders/) used by the optional LLM adapter passthrough (§8). The provider implementation catalog is now loaded through the unified adapter plugin bundle instead of a WebServiceProjectReference. The catalog registers plugins forllama-serverandollama(on-prem) and foropenai,claude, andgemini(cloud) only when the mounted/configured adapter bundle is present and admitted by the current bridge. The cloud providers are opt-in only: the LLM adapter is disabled by default (AdvisoryAI:Adapters:Llm:Enabled=false), provider configs are loaded from per-provider YAML inetc/llm-providers, and thedummyprovider is registered only when local-harness fakes are allowed. No cloud provider is the default and none is reachable until an operator explicitly enables the adapter, supplies a provider config, and mounts/configures the adapter bundle. Remote inference is additionally blocked in sealed/air-gapped mode (§14).
14) Controlled conversational interface and tool gating
- Chat Gateway controls. Chat endpoints enforce Authority auth, per-tenant/user quotas, token budgets, and PII/secret scrubbing before any model invocation.
- Grounded QA boundary. Chat context is assembled from the evidence bundle plus Knowledge Search and Unified Search results as the BitRAG substitute. The model must return structured JSON with cited claims; invalid or uncited output maps to controlled unavailability, not a fallback answer.
- Sanctioned tools only. Tool calls are schema-bound and allowlisted (read-only by default). Action tools require explicit user confirmation plus policy allow.
- Policy lattice. Tool permissions are evaluated against policy rules (scope, tenant, role, resource) before invocation.
- Audit log. Persist prompt hash, redaction metadata, tool calls, policy decisions, and model identifiers to Postgres; optional DSSE signatures capture evidence integrity.
- Offline parity. Local model profiles are the default; remote inference is opt-in and blocked in sealed mode.
- Action execution boundary.
IActionExecutordoes not simulate action success. After policy and approval checks, it dispatches only to registeredIActionConnectorimplementations. If no connector can execute the action type, the result isFailedwithACTION_CONNECTOR_UNAVAILABLEand anExecutionFailedaudit entry. Connector success is the only path that records idempotent successful execution. Rollback is also connector-gated: the executor looks up the original executed audit entry and returnsACTION_EXECUTION_NOT_FOUND,ACTION_ROLLBACK_NOT_SUPPORTED, orACTION_ROLLBACK_CONNECTOR_UNAVAILABLErather than pretending compensation exists. - GitHub remediation PR boundary.
GitHubPullRequestGeneratorcreates PR bodies only after an SCM connector and repository owner/repo context are valid. Missing connector/configuration, status lookup without persisted repository context, delta verdict update, and close operations fail closed instead of returning placeholder PR text, “waiting for CI” status, or silent no-ops.
See docs/modules/advisory-ai/chat-interface.md and docs-archive/product/advisories/13-Jan-2026 - Controlled Conversational Interface.md.
15) OpsMemory (Operational Memory and RAG)
Consolidated from
src/OpsMemory/intosrc/AdvisoryAI/(Sprint 213, 2026-03-04). Archived docs:docs-archive/modules/opsmemory/.
Overview
OpsMemory provides a decision ledger for security operations learning. It captures the complete lifecycle of a security decision – from situation context through action taken to eventual outcome – enabling playbook suggestions for future similar situations.
Source layout (post-consolidation)
- Library:
src/AdvisoryAI/__Libraries/StellaOps.OpsMemory/– core domain: models, similarity vectors, playbook suggestion engine, storage abstractions. - WebService:
src/AdvisoryAI/StellaOps.OpsMemory.WebService/– HTTP API (/api/v1/opsmemory/*), auth, Swagger, health checks. Deploys as its own container (opsmemory-web). - Tests:
src/AdvisoryAI/__Tests/StellaOps.OpsMemory.Tests/– unit (similarity vectors, playbook suggestions, context enrichers, chat provider) and integration (Postgres store with Testcontainers).
Key components
| Component | Purpose |
|---|---|
SimilarityVectorGenerator | 50-dimensional feature vectors from CVE, severity, reachability, EPSS/CVSS, component type, context tags |
PlaybookSuggestionService | Confidence-ranked suggestions from historical decisions |
OutcomeTrackingService | Records decision outcomes for feedback loop |
PostgresOpsMemoryStore | Postgres storage with array-based cosine similarity (no pgvector dependency) |
OpsMemoryChatProvider | Chat integration for conversational playbook queries |
OpsMemoryContextEnricher | Enriches AdvisoryAI context packs with operational memory |
API surface
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/opsmemory/decisions | Record a new decision (Write) |
| GET | /api/v1/opsmemory/decisions/{memoryId} | Get decision details (Read) |
| POST | /api/v1/opsmemory/decisions/{memoryId}/outcome | Record outcome (Write) |
| GET | /api/v1/opsmemory/suggestions | Get playbook suggestions (Read) |
| GET | /api/v1/opsmemory/decisions | Query past decisions (Read) |
| GET | /api/v1/opsmemory/stats | Get statistics (Read) |
Route group requires
OpsMemoryPolicies.Read; write paths (POST /decisions,POST /decisions/{memoryId}/outcome) additionally requireOpsMemoryPolicies.Writeand are.Audited(...). Path parameter ismemoryId(OpsMemoryEndpoints.MapOpsMemoryEndpoints).
Tenancy contract (claim-bound)
Auth-model note:
opsmemory-webdoes not run the §7.5 envelope stack. It authenticates as an Authority resource server (AddStellaOpsResourceServerAuthentication, i.e. a raw JWT bearer) and is reachable over the backend bypass networks, soInboundIdentityHeaderStripMiddlewareandUseIdentityEnvelopeAuthentication()— the protections §7.5 describes — do not apply here. The tenancy guarantee below is what isolates this host, and it is enforced in the host itself (Program.cs→UseStellaOpsTenantMiddleware+.RequireTenant()), not at the gateway.
The isolation tenant is resolved exclusively from the authenticated stellaops:tenant claim (OpsMemoryTenantResolver → IStellaOpsTenantAccessor, populated by the shared tenant middleware). Caller-supplied tenant input is never an isolation key:
- The
tenantIdquery parameter (GET /decisions,GET /decisions/{memoryId},GET /stats,GET /suggestions,POST /decisions/{memoryId}/outcome) and thetenantIdbody field ofRecordDecisionRequestare optional conflict checks. When present and different from the claim, the request is rejected with400/error_code: tenant_conflict(accept-but-verify: a mismatched caller fails loudly rather than being silently re-scoped). - A request with no tenant claim is rejected by the route group’s
.RequireTenant()filter with400/error_code: tenant_missing. This is load-bearing on the compose bypass networks (Authority__ResourceServer__BypassNetworks), whereStellaOpsBypassEvaluatorcan satisfy the scope policy with no principal at all: such a caller carries no tenant claim and is refused, instead of being served whichever tenant it named. - Regression coverage:
StellaOps.OpsMemory.Tests/Integration/OpsMemoryTenancyEndpointTests.cs(a tenant-A principal asking for tenant-B is refused on both read and write paths, and the store is never invoked with the foreign tenant).
Landed in Sprint SPRINT_20260712_001 (TEN-1); before it, every endpoint passed the caller’s tenantId straight to PostgresOpsMemoryStore as the sole isolation key.
Database
OpsMemory uses the shared Postgres instance with an opsmemory schema (OpsMemoryRuntimePersistenceExtensions.DefaultSchemaName = "opsmemory"). It is not EF Core; the schema is plain SQL embedded in __Libraries/StellaOps.OpsMemory/Migrations/001_initial_schema.sql and auto-migrated on startup via AddStartupMigrations<PostgresOptions>(...) (consistent with the repo-wide auto-migration mandate, not a manual init script). Tenant isolation is enforced at the query level (WHERE tenant_id = @tenantId), and the @tenantId the store receives is always the claim tenant — see the tenancy contract above; it is never taken from caller input. Similarity search uses array-stored vectors with in-store cosine similarity — no pgvector dependency.
Connection contract:
- Connection resolution precedence:
ConnectionStrings:OpsMemory->OpsMemory:Storage:ConnectionString->ConnectionStrings:Default. - Missing DB configuration is a startup error (fail-fast); localhost fallback is limited to development-only workflows.
Dependencies
StellaOps.Findings.Ledger(upstream library)StellaOps.Auth.ServerIntegration(authentication)StellaOps.Determinism.Abstractions(deterministic time/GUID providers)StellaOps.Localization(i18n)- AdvisoryAI core references OpsMemory via ProjectReference for context enrichment
