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.
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 live on its own database. What runs isadvisoryai-web+advisoryai-worker, two replica roles of one deployable family onstellaops_advisoryai, composed fromdevops/compose/docker-compose.advisoryai.yml.Verified against commit
79bf4c6f36(the retirement) plus live probes taken 2026-09-07 on the running estate. Re-verify with:docker inspect stellaops-advisoryai-web stellaops-advisoryai-worker --format '{{.Name}} {{.Image}} {{.State.Health.Status}} restarts={{.RestartCount}}' docker exec stellaops-advisoryai-web sh -lc 'cat /app/buildinfo.json' rg -n "^ advisoryai-(web|worker):" devops/compose/docker-compose.advisoryai.ymlAt that stamp both containers were
healthywithRestartCount=0on imagessha256:d423274f669b…andsha256:4fadbb883b7c…, built fromgitSha 3c938e875f…withworktreeState clean.
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”).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.
- 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. This source contract is ready for the AAI-9 window but is not evidence that the live data move/deploy has occurred. - 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): 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,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).
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) — 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). 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 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 (
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 until the AAI-9 cutover enables it and verifies the registry row. - 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. 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.- 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 becauseadvisoryai-webis already reachable by container DNS alias (advisoryai.stella-ops.local) and requires the gateway identity envelope. advisoryai-webregistersAddAdvisoryAiRuntimePersistence, andadvisoryai-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.
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 | 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 -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 devops/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.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
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. __Libraries/StellaOps.OpsMemory/Migrations/001_initial_schema.sql is no longer applied by anything: the host that ran it (AddStartupMigrations<PostgresOptions>(...)) was retired on 2026-09-07. 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:
- Pre-cutover host connection precedence:
ConnectionStrings:OpsMemory->OpsMemory:Storage:ConnectionString->ConnectionStrings:Default. - Consolidated host: only
STELLAOPS_POSTGRES_ADVISORYAI_CONNECTION(orPostgres:AdvisoryAI:ConnectionString); no OpsMemory/legacy/generic fallback. - 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
