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.
Verified against commit
942a18727con 2026-09-26 (UTC). Re-verify:grep -nE "^ advisoryai-(web|worker|data-init):" devops/compose/docker-compose.advisoryai.yml devops/release/bundle/docker-compose.ymlandls src/AdvisoryAI/__Libraries/StellaOps.AdvisoryAI.Persistence/Migrations/.This dossier describes the CONSOLIDATED family only. The predecessor deployables
advisory-ai-web,advisory-ai-workerandopsmemory-webwere retired on 2026-09-07 (SPRINT_20260722_013AAI-10) after the AAI-9 data-move window of 2026-09-04 put the family on its own database. What runs isadvisoryai-web+advisoryai-worker, two roles of one deployable family onstellaops_advisoryai, composed from the family overlaydevops/compose/docker-compose.advisoryai.yml, which sits outside the default compose chain. The release bundle (devops/release/bundle/docker-compose.yml) does not deploy Advisory AI. Live container probes recorded on 2026-09-07 are history; this revision re-verified source and compose only.
Tenant identity (claim-bound). The family ships one host with an inbound API surface, and it takes 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.
advisoryai-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). The lifted OpsMemory routes ride that same stack: the/api/v1/opsmemoryand/api/advisoryai/v1/opsmemorygroups carry.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 an identity with no tenant is400 tenant_missing(see §15, “Tenancy contract”).- Conversations, chat, consent and runs (
SPRINT_20261018_009PCD-866). The legacy/v1/advisory-ai/conversations*routes, the/api/v1/chatand/v1/advisory-ai/runsgroups and the three consent routes carry.RequireTenant()and read the tenant fromIStellaOpsTenantAccessor. A principal without a tenant gets400 tenant_missingand writes nothing; the former"default"fallback is gone. The conversation create body and list query carry no tenant. Every conversation read, turn, context update and delete addresses the row by tenant and ID (ConversationStoreopens its session with the tenant and filters ontenant_id), so another tenant’s conversation ID answers404with no body, the same as an unknown ID. The assistant turn runtime receives the caller’s tenant only. Asserted on real PostgreSQL byStellaOps.AdvisoryAI.Tests.Integration.ConversationTenantBindingTests.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 guarantee is now asserted on the consolidated host byStellaOps.AdvisoryAI.Tests.Integration.AdvisoryAiOpsMemoryTenancyTests(15 facts, all six routes, read and write paths, both roots). 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 endpoints hand 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/. - Consolidated PostgreSQL database (auto-migrated). Current source embeds the immutable
001_advisoryai_consolidated_baseline.sqlplus forward migrations undersrc/AdvisoryAI/__Libraries/StellaOps.AdvisoryAI.Persistence/Migrations/, all under oneadvisoryai.schema_migrationsauthority.advisoryai-webregisters that authority behind the own-connection gate and before either knowledge indexer; the worker registers runtime DAL only. The legacy active baseline andPostgresKnowledgeSearchStore.EnsureSchemaAsyncprivate ledger were retired together by AAI-9 gates 1–2. The pre-1.0 files remain historical underStorage/Migrations/_archived/pre_1.0/mig061/and are not embedded. The current table families include:- 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). - Evidence Packs: forward migration
002_advisoryai_evidence_pack_store.sqladdsadvisoryai.evidence_packs(full aggregate plus current DSSE-signed aggregate) and tenant-boundadvisoryai.evidence_pack_run_links. Pack content is immutable by ID, signed content must equal the stored aggregate, and both tables aresource-of-truthretention class. - Retention classes (
SPRINT_20260916_001RFC-13, verified 2026-09-23; receiptretention-classification-verification-20260923.json): All 27 tables carry a class earned from their writers and readers. Forward migration004statesderived-rebuildablefor the five RAG and registry projections thatReplaceIndexAsyncrebuilds, andsource-of-truthfor the run, explanation, policy-intent, chat-settings, consent, Evidence Pack and OpsMemory tables.runtime_runsholds the signed run attestation, and an explanation id hashes the model output, so neither can be regenerated.005statescache-boundedfor the four search telemetry tables, whichSearchAnalyticsRetentionBackgroundServiceages out afterSearchAnalyticsRetentionDays(default 90).doctor.advisoryai.retention.search-telemetryreports that window and itsSearchAnalyticsRetentionEnabledswitch.006makesconversationsandturnsoperational-state:advisoryai.prune_idle_conversationsreclaims a conversation idle pastConversationOptions.ConversationRetention(7 days) together with its turns. It locks the idle rows first, so a concurrent turn cannot leave a partial history. The web host’sConversationRetentionServicecalls it hourly, anddoctor.advisoryai.retention.conversation-statechecks it. The chat audit lineage keeps its 90-day window from003.007statesderived-rebuildableforentity_aliasunder owner ruling Q-41: the unified indexer re-derives it from adapter-declared identities on every pass and rebuild (see “Cross-identifier alias resolution”). Re-verify with the RFC-13 test classesSearchTelemetryRetentionTests,ConversationRetentionTests,EntityAliasIndexingTestsandAdvisoryAiRuntimeBindingGuardTests, and withcheck-p13-retention.py --tables.
- Consent & attestation:
- All runtime PostgreSQL stores, including
AdvisoryAI:Storage,AdvisoryAI:KnowledgeSearch, chat audit, andIEvidencePackStore, resolve the same service-owned DSN fromSTELLAOPS_POSTGRES_ADVISORYAI_CONNECTION(orPostgres:AdvisoryAI:ConnectionString). The Evidence Pack pool is private and does not claim the host-wide bareNpgsqlDataSourceslot. Legacy and generic connection values are not fallbacks. When the own DSN is absent, production-like hosts fail closed; an explicit local harness may use its named fakes, but Evidence Pack storage remains unconfigured unless the harness explicitly replaces it. The AAI-9 window and AAI-10 predecessor-host retirement are recorded in the archived consolidation sprint; subsequent source changes require their own runtime verification. - Re-verify: run
PostgresEvidencePackStoreTests,AdvisoryAiDurableRuntimeTests.EvidencePack_PersistsAcrossHostRestart_AndNativeEndpointsUseOwnedStore,AdvisoryAiDurableRuntimeTests.EvidencePack_ProductionComposition_AllowsUnsignedOperations_AndSigningFailsOnUse, andAdvisoryAiRuntimeStartupContractTests.WebHost_LocalHarnessWithoutOwnDatabase_KeepsEvidencePackStorageFailClosedwithtools/scripts/test-targeted-xunit.ps1, then run the fullStellaOps.AdvisoryAI.Persistence.Testsassembly. - 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. The durable integration harness proves that, with the own DSN configured and its explicit local fake signer enabled, create/get/list/run/export data survives host restart through the AdvisoryAI-owned PostgreSQL store. Commit7d30dbf4ea79cba79569413839237c6ac84e3347closes the Production composition gap found by the earlier isolated boot: Production now resolvesUnconfiguredEvidencePackSigner, allowing unsigned storage operations while retaining fail-closed sign/verify behavior. Verified-by:pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj -Method "*EvidencePack_ProductionComposition_AllowsUnsignedOperations_AndSigningFailsOnUse*" -BuildProjectReferencesboots a Production host against a fresh own database withAllowLocalHarnessFakes=false, returns list=200, persists an unsigned pack and rejects sign/verify (1 selected / 933 suite). This is source/TestServer evidence; a clean successor-image direct-host re-force remains before any runtime or signed-export claim. The store’s*tenant lookup is an internal service seam; Evidence Pack HTTP handlers reject a wildcard tenant claim before storage.
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): AdvisoryAI registers no header-trust handler.AdvisoryAiHeaderAuthenticationHandler, which accepted rawX-User-Id,X-Tenant-Id, andX-StellaOps-Scopesheaders as authoritative identity, is deleted as a security hardening; the 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 send 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 are 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,findings-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.findings-internalis its own client rather than a rename ofscanner-internal, because that one still has real scanner consumers (FederatedSearchDispatcher, the explanation runtime bindings) that did not move service. - Config keys:
Enabled,Issuer,MetadataAddress,TokenEndpoint,ClientId,ClientSecret,Scope(defaultadvisory:read findings:read graph:read policy:read vex:read),Tenant(defaultdefault),BackchannelTimeoutSeconds.TokenEndpointis optional but load-bearing whereverMetadataAddressis HTTP: unset, the token URL comes from the discovery document and OpenIddict refuses the resulting plain-HTTP token request (ID2083, “This server only accepts HTTPS requests”), which surfaces as a 400 inside the bearer handler and sends every adapter to its snapshot. Set it to the TLS-terminated token URL the estate accepts (https://router.stella-ops.local/connect/tokenin the reference compose, which also requires the calling container to trust the router CA), the same wayDoctor:Registration:TokenEndpointis set. The worker’s copy of this block reads the same key. Pinned byAdvisoryAiAuthorityClientTests. 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).
Unified-search live adapter targets and provenance
Each live adapter calls one route on one service, and a wrong target does not fail a query — the adapter catches the failure and answers from its committed snapshot, so the corpus reads as healthy while it is a fixture. That is what let three separate upstream defects run unnoticed for a month (SPRINT_20260722_013 AAI-15).
| Adapter | Route | Service | Scope | Notes |
|---|---|---|---|---|
FindingsSearchAdapter | GET /api/v2/security/findings | findings-web (findings.stella-ops.local) | findings:read | MapSecurityReadModelEndpoints has exactly one live caller, StellaOps.Findings.WebService. The projection’s identifier field is vulnerabilityId, not cveId (FND-25), so the mapper reads that first. |
PolicySearchAdapter | GET /api/v1/authority/effective-policies?enabledOnly=false&includeExpired=true&limit=100 | policy-engine | policy:read | Policy Engine maps no bulk gate-decision list; the previous /api/v1/gates/decisions matched /{bomRef} with bomRef = "decisions". The adapter states every filter (AAI-17) because its mapper renders disabled and expired enforcement, which the route’s own defaults would exclude. |
VexSearchAdapter | GET /api/vulnerabilities/v1/corpus/publications/current, then the publication’s consensus-inputs section by its declared href | vulnerabilities-web (vulnerabilities.stella-ops.local) | advisory:read | Per VULN-G3 D-G3-1 (SPRINT_20260722_008): bulk enumeration is bootstrap-from-publication. The consensus-inputs rows (vulnerability, product, issuer, status, justification, applicability) are the VEX inputs behind the hub consensus; the adapter streams at most 1,000 of them per refresh, validates the section’s format version and self-declared header, and reports corpus_publication_not_ready as a state (snapshot, not suppressed) rather than a missing route. The retired target GET /api/v1/canonical (Concelier) paged canonical advisories and called them VEX statements. |
Provenance is a first-class surface, not a log line:
GET /v1/search/adapters(tenant context,advisory-ai:operate) returns per adapter the source (Upstream,Snapshot,Unconfigured,Disabled), the cause when it is not upstream, the attempted target, the chunk count, andUpstreamSuppressedUntil. The same list rides on thePOST /v1/search/index/rebuildresponse.- Counters
advisory_ai_unified_adapter_ingest_total{domain,adapter,source}andadvisory_ai_unified_adapter_fallback_total{domain,adapter,reason,structural}on meterStellaOps.AdvisoryAI. - A
404marks the route as structurally absent and suppresses the upstream call for five minutes, doubling to a one-hour ceiling while it stays absent and released on the first success — so the periodic refresh stops re-issuing a call that cannot succeed.
Verified-by: UnifiedSearchLiveAdapterIntegrationTests (route targets, the vulnerabilityId mapping, provenance recording, the 404 backoff and its expiry) and UnifiedSearchEndpointsIntegrationTests (the status endpoint’s scope and tenant gates).
Cross-identifier alias resolution
Unified search resolves an identifier to the entity another adapter indexed it under. The resolution table is advisoryai.entity_alias, and the unified indexer is its only writer (SPRINT_20260916_001 RFC-13, owner ruling Q-41, 2026-09-23).
- Input. An adapter declares a second identity of a chunk’s entity in the chunk metadata property
entity_aliases, as entity keys. The scan adapter declares the image reference it scanned (image:<ref>); a scan without a declared reference declares nothing. The graph adapter declares an image node’s content digest (image:<digest>). An adapter whose upstream carries advisory aliases declares them the same way, for exampleghsa:GHSA-…on acve:CVE-…chunk. No current upstream payload carries a CVE-to-GHSA pair, so these two image families are what production fills today. - Rows. For each declared alias the indexer registers the alias to the entity key, the bare identifier (the text after the scheme prefix, which is what a user types) to the entity key, the entity key to the alias, and one self row for the entity key.
- Readers. A query that matches a registered alias gets an
entity_aliasrefinement naming the canonical key, with the candidate count the index returns for it. The entity card assembler groups a card keyed by an alias under the canonical key, lists the aliases as the card’s connections, and keeps a key that resolves to itself canonical. - Tenancy. Only tenant-neutral chunks contribute (metadata
tenantabsent orglobal). The table has no tenant column and its readers pass no tenant, so an identity from a tenant-scoped chunk would otherwise reach every tenant. - Rebuild. Every index pass and every rebuild (the startup rebuild and
POST /v1/search/index/rebuild) re-derives each domain’s rows in the same per-domain step that writes itskb_docandkb_chunkrows.EntityAliasService.RegisterAliasAsyncupserts each (alias, entity key) pair, so re-ingesting the same documents adds nothing.RetainSourceAliasesAsyncthen deletes the rows that domain registered (source = unified-index:<domain>) and no longer declares. An emptied table is restored by the next pass. The knowledge rebuild (POST /v1/advisory-ai/index/rebuild) does not feed the table, because shipped documents, specifications and Doctor checks declare no aliases.
Verified-by: EntityAliasIndexingTests (real migrated schema: both resolution directions, search by the alias id, idempotent re-ingestion, rebuild of an emptied table, removal of an alias no longer declared, the two production image families, tenant exclusion) and EntityCardAssemblerTests.
8) APIs
All HTTP routes are mapped in src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs (plus the per-feature Endpoints/*.cs). AAI-9 gate 3 adds native /api/advisoryai/v1/** aliases after the entire optional endpoint surface has been composed; every alias retains its handler, HTTP methods, authorization/rate-limit metadata and response behavior. The legacy /v1/advisory-ai/*, /api/v1/chat/*, /v1/search/*, /v1/evidence-packs/*, /v1/runs/*, and /api/v1/opsmemory/* routes remain in the same image for rollback until AAI-10. Gate 9 adds the six OpsMemory handlers at /api/advisoryai/v1/opsmemory/** with suffixed endpoint names and request-native decision Location links. This is source readiness only: the gateway swap and a clean digest-pinned continuation image are still pending. 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;.RequireTenant()) — durable consent underadvisoryai.ai_consents, keyed by the caller’s tenant and subject.POST /v1/advisory-ai/justify(Operate) — VEX justification draft.GET /v1/advisory-ai/rate-limits(View).
Chat gateway (/api/v1/chat/*, all Operate, group .RequireTenant())
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 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. The roles passed to the action policy gate are not parsed here: both the conversation and the/api/v1/chat/querypaths take them fromSecurity/VerifiedActionRoles, which reads only theClaimTypes.Roleclaims of the identity built from the gateway-signed envelope (authentication typeStellaRouterEnvelope); no request body field or header supplies a role (SPRINT_20261006_114 PCD-904).
Runs ledger (/v1/advisory-ai/runs, group View and .RequireTenant(); 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). Item reads own thepack-*identifier namespace: an identifier outside that namespace returns404before storage is invoked, while a real pack-store failure remains a5xx. Tenant comes only from the authenticated envelope; empty and wildcard tenant claims return401, and run-link results are filtered back to that tenant before projection.
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 mounted-adapter drop-points go through the shared signed admission:AdvisoryAiLlmAdapterPluginBridgeloads the mounted unified adapter in place of a bareLoadFromAssemblyPath, 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 (
advisoryai-web) - exposes the full dual-prefix API surface in §8 (notably legacyPOST /v1/advisory-ai/pipeline/{taskType}and nativePOST /api/advisoryai/v1/advisory-ai/pipeline/{taskType}) to materialise plans and enqueue execution messages. Plan materialisation runs the structured retriever in-process, so since 2026-09-10 (SPRINT_20260722_013 AAI-5) the web role binds the same hub-backedHubAdvisoryDocumentProvideras the worker wheneverAdvisoryAI:DocumentProvider:Hub:BaseAddressis configured; a Production host without the key keeps the fail-closed fallback and the pipeline routes answer 400 rather than an empty plan. - Worker (
advisoryai-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 bindsHubAdvisoryDocumentProviderbeforeAddAdvisoryAiCore, reads the Vulnerabilities owner API’s consensus projection, and uses the establishedstellaops-advisory-ai-internalAuthority client. It does not read Concelier persistence. - Doctor registration identity. The same
stellaops-advisory-ai-internalclient is grantedplatform:doctor:register; the host must not configure a second Doctor client because its outbound adapters share oneStellaOpsAuthClientOptionsregistration. The live Authority row and a fresh default-tenant scoped token were reconciled and verified on 2026-08-22 without changing the existing audiences or adapter scopes. Capability registration stays default-off: the overlay setsDoctor__Registration__EnabledfromADVISORYAI_DOCTOR_REGISTRATION_ENABLED(defaultfalse). - 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
advisoryai-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.
Continuation image/source contract (PCD-424: overlay source e11ddef1e8, conformance implementation 698b0f5ec9). The estate overlay devops/compose/docker-compose.advisoryai.yml selects a reviewed immutable image separately for advisoryai-web and advisoryai-worker. Its data-init image is not evidence for either serving role. The publish helper maps each key to its corresponding src/AdvisoryAI/StellaOps.AdvisoryAI.{WebService,Worker} project, assembly and successor image repository; both belong to the advisory-ai build family. The architecture conformance class checks those exact mappings and per-role pins, rejecting swapped or foreign valid-shaped digests, malformed values, mutable role tags and duplicate build keys, while retaining predecessor exclusions.
Selected cached-image metadata was inspected on 2026-10-06 UTC: the configured web and worker images have their corresponding APP_BINARY values and source revision 88280dc495; their entrypoint loads that binary. This bounded immutable-artifact check does not verify a running container, endpoint, database migration ledger or AdvisoryAI capability. Recheck the source contract with the canonical test wrapper and -Class "*AdvisoryAiConsolidationConformanceTests".
advisoryai-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.- Frontdoor routing: the gateway route table for AdvisoryAI was not re-verified on 2026-09-26; read the live gateway configuration before relying on any front-door path. The host registers with the Router as microservice
advisoryai(AddRouterMicroservice,Router__Enabledin the overlay). advisoryai-webregistersAddAdvisoryAiRuntimePersistence, andadvisoryai-workerregistersAddAdvisoryAiCoreRuntimePersistence; both bind durable PostgreSQL-backed consent, attestation, explanation replay, policy-intent, run, conversation, and chat-settings stores. Only the web role migrates:AddConsolidatedAdvisoryAiPersistenceregistersAddStartupMigrationsfor both schemas, and the worker never registers a migrator (asserted byAdvisoryAiRuntimeBindingGuardTests).- 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.
advisoryai-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 | .runtime/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 -SignAdvisoryAiBundles -UseOfflineDevSigner -OfflineDevSigningKeyRoot <installation-custody-root> stages the signed stellaops.advisoryai.llm-provider.ollama bundle under <profile>/llm-providers/. The cryptography library resolves the installation’s existing runtime-plugin purpose key; missing custody fails instead of silently generating a replacement. Explicit enrollment and lost-key rotation follow Signing keys by purpose. The loader already handles multiple providers; staging the remote provider and adapter bundles is the documented follow-up. The exported public cosign.pub is git-ignored under the AdvisoryAI trust-root directory.
Compose mounts: the advisoryai-web definition in devops/compose/docker-compose.advisoryai.yml carries the read-only mounts of .runtime/plugins/advisoryai/base + the trust root, keeps the LLM adapter enabled, restates the loader defaults (ADVISORYAI__AdvisoryAI__LlmProviders__RuntimePlugins__RootPath/Profile/TrustRootPath) and names the writable scratch volume explicitly. That contract was written onto the continuation key at AAI-9 gate 4 precisely because the predecessor definition it replaced lived in another file, and it is now the only copy: AAI-10 deleted the predecessor key from devops/compose/docker-compose.stella-services.yml on 2026-09-07. An earlier separate mount overlay had already been retired after Linux Compose 2.39 rejected its duplicate imported-service definition.
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 canonical Compose mounts are committed, and
/internal/plugins/status+/internal/plugins/probeare mapped, but a live canonical-stack acceptance probe against a runningadvisoryai-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.
advisoryai-webexposes the API/plan cache endpoints whileadvisoryai-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.
advisoryai-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
advisoryai-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 publishes the AdvisoryAI web and worker images 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. - Application:
src/AdvisoryAI/__Libraries/StellaOps.OpsMemory.Application/– the HTTP surface (/api/v1/opsmemory/*and/api/advisoryai/v1/opsmemory/*), the tenancy resolver and the scope policies. Mapped in-process byadvisoryai-web; it is a module, not a deployable. - Retired host:
src/__Obsoleted/AdvisoryAI/StellaOps.OpsMemory.WebService/– the formeropsmemory-webcontainer, frozen 2026-09-07 (AAI-10, CoC §15.4). It was a composition root with no routes of its own. - 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) |
advisoryai-web serves all six on both roots: the legacy /api/v1/opsmemory/** above and the native /api/advisoryai/v1/opsmemory/**. Route names on the native aliases use the .consolidated suffix, so create responses generate a Location under the prefix the caller used. The pairing is asserted route by route – method, endpoint name, authorization policy and audit metadata – by AdvisoryAiOpsMemoryEndpointTests.HostCarriesAllSixLegacyAndNativeRoutes_WithEquivalentNamesAuthAndAuditMetadata.
Route group requires
OpsMemoryPolicies.Read; write paths (POST /decisions,POST /decisions/{memoryId}/outcome) additionally requireOpsMemoryPolicies.Writeand are.Audited(...). Path parameter ismemoryId(OpsMemoryEndpoints.MapOpsMemoryEndpoints). Those policy names readops-memory:read/ops-memory:writefrom the signed gateway envelope.
Tenancy contract (claim-bound)
Auth-model note, corrected by the retirement (2026-09-07). Until AAI-10 these routes ran on a second host that did not use the §7.5 envelope stack:
opsmemory-webauthenticated as an Authority resource server (a raw JWT bearer) reachable over the backend bypass networks, so the header strip and envelope authentication §7.5 describes did not apply to it. That host is gone. The routes now run insideadvisoryai-weband inherit §7.5 in full — envelope-only identity, the inbound header strip, andUseStellaOpsTenantMiddleware+.RequireTenant()on the groups. The tenancy guarantee below is enforced in the endpoints themselves, so it holds on either root.
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.AdvisoryAI.Tests/Integration/AdvisoryAiOpsMemoryTenancyTests.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
The opsmemory schema now lives in the family’s own database stellaops_advisoryai, created by the consolidated baseline, which carries opsmemory.decisions alongside the AdvisoryAI tables. There is exactly one migration authority: advisoryai-web registers the folded store/playbook/chat services against the same service-owned DSN as every other AdvisoryAI store, without a second migrator, and the OpsMemory pool stays private to the module so the bare NpgsqlDataSource DI slot is free. The predecessor 001_initial_schema.sql is frozen unchanged at src/__Obsoleted/AdvisoryAI/__Libraries/StellaOps.OpsMemory/Migrations/. EST-9 removes its embedding and the unused startup helper; OpsMemory tests converge the canonical owner chain and verify complete-ledger parity across restart. The legacy stellaops_platform.opsmemory schema is retained offline as a rollback input until the separately approved destructive drop. 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:
- Consolidated host: only
STELLAOPS_POSTGRES_ADVISORYAI_CONNECTION(orPostgres:AdvisoryAI:ConnectionString); no OpsMemory/legacy/generic fallback. - Missing owner configuration fails startup outside the explicitly configured local harness; the removed OpsMemory helper supplies no fallback.
Dependencies
StellaOps.Auth.ServerIntegration(authentication)StellaOps.Determinism.Abstractions(deterministic time/GUID providers)StellaOps.Localization(i18n)- AdvisoryAI core references OpsMemory via ProjectReference for context enrichment
