Advisory AI architecture

Captures the retrieval, guardrail, and inference packaging requirements defined in the Advisory AI implementation plan and related module guides. Configuration knobs (inference modes, guardrails, cache/queue budgets) now live in docs/modules/policy/guides/assistant-parameters.mdper DOCS-AIAI-31-006.

Tenant identity (claim-bound). The module ships two hosts with different inbound auth, and both take the data-isolation tenant from the authenticated stellaops:tenant claim. A caller-supplied tenant is never an isolation key — it is the cross-tenant vector, not a safety property.

  • advisory-ai-web (StellaOps.AdvisoryAI.WebService) — envelope-only. Identity resolves exclusively from the gateway-signed identity envelope; InboundIdentityHeaderStripMiddleware strips X-Tenant-Id-style headers at ingress before UseAuthentication(), and the legacy AdvisoryAiHeaderAuthenticationHandler has been deleted (see §7.5).
  • opsmemory-web (StellaOps.OpsMemory.WebService) — resource-server bearer + claim-bound tenancy. AddStellaOpsResourceServerAuthentication + UseStellaOpsTenantMiddleware; the /api/v1/opsmemory group carries .RequireTenant(), and every handler isolates on the claim resolved by OpsMemoryTenantResolver. The tenantId query parameter and the RecordDecisionRequest.TenantId body field are optional conflict checks only: disagreeing with the claim is 400 tenant_conflict, and a request with no tenant claim is 400 tenant_missing (see §15, “Tenancy contract”).

Landed in Sprint SPRINT_20260712_001 (TEN-1). Before it, OpsMemory endpoints passed the caller’s tenantId straight to PostgresOpsMemoryStore as the sole isolation key and never consulted the claim — on the compose bypass networks that was an unauthenticated cross-tenant read/write. The library-level guard in Playbook/PlaybookSuggestionService (throws when handed a blank tenant; the legacy shared-"default" fallback is gone) is defence-in-depth for internal callers of the library — the value the WebService hands it is always the claim tenant.

1) Goals

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

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

5) Deterministic tooling

6) Output persistence

7) Profiles & sovereignty

7.5) Authentication & authorization (Sprint SPRINT_20260430_002)

Audit finding A2 (docs-archive/qa/audits/microservice-audit-pass2-2026-04-29.md): the previously registered AdvisoryAiHeaderAuthenticationHandler accepted raw X-User-Id, X-Tenant-Id, and X-StellaOps-Scopes headers as authoritative identity. That handler has been deleted as a security hardening; the current contract is gateway-envelope-only.

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:

8) APIs

All HTTP routes are mapped in src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs (plus the per-feature Endpoints/*.cs). The canonical route prefix is /v1/advisory-ai/*; chat lives under /api/v1/chat/* and unified search under /v1/search/*. There is no /api/v1/advisory/* surface — that path is historical and does not exist in code. Every business endpoint is authorized by a named policy (§7.5) and rate-limited by the advisory-ai token bucket (30 req/min per X-StellaOps-Client); write paths are wrapped with .Audited(...).

Authorization policies resolve scopes via StellaOpsScopes: advisory-ai:view (View), advisory-ai:operate (Operate, implies View), advisory-ai:admin (Admin, implies Operate). The legacy advisory:run / advisory:explain / advisory:companion / advisory:remediate / advisory:justify strings still appear in some handler-internal EnsureAuthorized checks, but those strings are not registered Authority scopes — modern gateway envelopes carry the advisory-ai:* scopes that the named policies enforce first.

Pipeline & outputs

Explanation & companion

Remediation (Remedy Autopilot)

Policy Studio (Copilot)

Consent, justification, rate limits (VEX-AI)

Chat gateway (/api/v1/chat/*, all Operate)

Runs ledger (/v1/advisory-ai/runs, group View; mutations Operate, audited)

Attestations & evidence packs

LLM adapter passthrough (only when AdvisoryAI:Adapters:Llm:Enabled=true)

Health & infra

Pipeline plan/output responses carry output_hash, input_digest, and citations for verification.

9) Observability

10) Operational controls

11) Hosting surfaces

11.1) Mounted LLM provider plugin loader (signed bundle admission)

MountedLlmProviderRuntimePluginLoader (src/AdvisoryAI/StellaOps.AdvisoryAI/Inference/LlmProviders/Admission/MountedLlmProviderRuntimePluginLoader.cs, commit a36f672403) discovers, admits, and activates signed per-provider ILlmProviderPlugin bundles from a mounted profile directory and registers the survivors in LlmProviderCatalog so they flow through the existing unified LlmPluginAdapter. Only the AdvisoryAI web service runs the provider loader (the worker does not), and the LLM adapter surface must be enabled (AdvisoryAI:Adapters:Llm:Enabled, default true in the base stack) for mounted providers to be admitted. The loader is fail-closed and de-duplicates by ILlmProviderPlugin.ProviderId (a duplicate is surfaced as rejected, never silently shadowed). A missing/unmounted root is fail-open: AdvisoryAI keeps running with only the built-in providers.

advisory-ai-web also maps the canonical pluginized-compose diagnostics: GET /internal/plugins/status returns the loader’s catalog state, and POST /internal/plugins/probe runs a deterministic catalog probe that marks mounted, discovered, admitted, and loaded provider bundles as responded. The probe does not call an external model endpoint.

Admission chain — each bundle is admitted through the shared SignedRuntimePluginAdmission chokepoint (src/__Libraries/StellaOps.Plugin/Security/SignedRuntimePluginAdmission.cs, promoted from AdvisoryAI in commit c1922c1ce5; AdvisoryAI keeps a thin facade that bakes in module advisoryai, contract runtime-bundle.v1, and capability advisoryai:llm-provider):

  1. Manifest bindingid equals the bundle directory name, module is advisoryai, contractVersion is runtime-bundle.v1, the configured profile matches (when non-empty), capability advisoryai:llm-provider is declared, and a well-formed assembly descriptor (relative path + sha256) is present.
  2. 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>.sig is enforced to the conventional location so a tampered manifest cannot redirect the verifier.
  3. Detached RSA-PKCS1-SHA256 verificationOfflineDevRsaSha256PluginVerifier with AllowUnsigned=false against the configured trust root. Only then is the ILlmProviderPlugin entry type activated via ActivatorUtilities. An AssemblyLoadContext resolving hook (AdvisoryAiPluginAssemblyResolver) resolves transitive StellaOps.* 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_):

KeyDefaultPurpose
AdvisoryAI:LlmProviders:RuntimePlugins:RootPath/app/plugins/advisoryaiRoot containing profile directories.
AdvisoryAI:LlmProviders:RuntimePlugins:ProfilebaseProfile; the loader resolves provider bundles under <RootPath>/<Profile>/llm-providers.
AdvisoryAI:LlmProviders:RuntimePlugins:TrustRootPath/app/etc/certificates/trust-roots/plugins/advisoryai/cosign.pubTrust-root public key.
AdvisoryAI:LlmProviders:RuntimePlugins:RequireReadOnlyBundlestrueReject 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:

PurposeHost pathContainer path
Signed LLM provider bundledevops/plugins/advisoryai/base/llm-providers/<provider-id>/ (manifest.json + <assembly>.dll + <assembly>.dll.sig)/app/plugins/advisoryai/base/llm-providers/<provider-id>
Operator config/registrydevops/etc/plugins/advisoryai//app/etc/plugins/advisoryai
AdvisoryAI plugin trust rootdevops/etc/certificates/trust-roots/plugins/advisoryai/cosign.pub/app/etc/certificates/trust-roots/plugins/advisoryai/cosign.pub
Probe scratchnamed volume advisoryai-plugin-scratch/var/lib/stellaops/plugin-scratch/advisoryai

Bundle producer: devops/build/package-runtime-plugins.ps1 -Module advisoryai -Profile base -UseOfflineDevSigner stages the signed stellaops.advisoryai.llm-provider.ollama bundle under <profile>/llm-providers/. The loader already handles N providers; staging the remote provider + adapter bundles is the documented follow-up. The generated cosign.pub is git-ignored under the advisoryai trust-root directory.

Opt-in compose overlay: devops/compose/docker-compose.plugins.advisoryai.yml layers read-only mounts of devops/plugins/advisoryai/base + the trust root onto advisory-ai-web, keeps the LLM adapter enabled, and restates the loader defaults (ADVISORYAI__AdvisoryAI__LlmProviders__RuntimePlugins__RootPath/Profile/TrustRootPath). Apply with COMPOSE_EXTRA_FILES=docker-compose.plugins.advisoryai.yml ./scripts/compose-cli.ps1 up.

Tests: focused loader admission coverage includes signed load, missing/unmounted root, bad hash, bad signature, duplicate provider ID, unsupported contract, capability mismatch, disabled manifest, writable mount, and malformed/path traversal cases; every reject path admits zero providers. The internal status/probe endpoints have integration coverage for status output, probe response marking, and plugin ID filtering.

Live runtime probe is pending. The loader, hardened bridges, producer, and overlay are committed, and /internal/plugins/status + /internal/plugins/probe are mapped, but a live overlay-up acceptance probe against a running advisory-ai-web (mount the signed Ollama provider, confirm GET /v1/advisory-ai/adapters/llm/providers and /internal/plugins/* report it admitted/responded) has not yet been recorded.

12) QA harness & determinism (Sprint 110 refresh)

13) Deployment profiles, scaling, and local model inference

14) Controlled conversational interface and tool gating

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/ into src/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)

Key components

ComponentPurpose
SimilarityVectorGenerator50-dimensional feature vectors from CVE, severity, reachability, EPSS/CVSS, component type, context tags
PlaybookSuggestionServiceConfidence-ranked suggestions from historical decisions
OutcomeTrackingServiceRecords decision outcomes for feedback loop
PostgresOpsMemoryStorePostgres storage with array-based cosine similarity (no pgvector dependency)
OpsMemoryChatProviderChat integration for conversational playbook queries
OpsMemoryContextEnricherEnriches AdvisoryAI context packs with operational memory

API surface

MethodPathDescription
POST/api/v1/opsmemory/decisionsRecord a new decision (Write)
GET/api/v1/opsmemory/decisions/{memoryId}Get decision details (Read)
POST/api/v1/opsmemory/decisions/{memoryId}/outcomeRecord outcome (Write)
GET/api/v1/opsmemory/suggestionsGet playbook suggestions (Read)
GET/api/v1/opsmemory/decisionsQuery past decisions (Read)
GET/api/v1/opsmemory/statsGet statistics (Read)

Route group requires OpsMemoryPolicies.Read; write paths (POST /decisions, POST /decisions/{memoryId}/outcome) additionally require OpsMemoryPolicies.Write and are .Audited(...). Path parameter is memoryId (OpsMemoryEndpoints.MapOpsMemoryEndpoints).

Tenancy contract (claim-bound)

Auth-model note: opsmemory-web does not run the §7.5 envelope stack. It authenticates as an Authority resource server (AddStellaOpsResourceServerAuthentication, i.e. a raw JWT bearer) and is reachable over the backend bypass networks, so InboundIdentityHeaderStripMiddleware and UseIdentityEnvelopeAuthentication() — the protections §7.5 describes — do not apply here. The tenancy guarantee below is what isolates this host, and it is enforced in the host itself (Program.csUseStellaOpsTenantMiddleware + .RequireTenant()), not at the gateway.

The isolation tenant is resolved exclusively from the authenticated stellaops:tenant claim (OpsMemoryTenantResolverIStellaOpsTenantAccessor, populated by the shared tenant middleware). Caller-supplied tenant input is never an isolation key:

Landed in Sprint SPRINT_20260712_001 (TEN-1); before it, every endpoint passed the caller’s tenantId straight to PostgresOpsMemoryStore as the sole isolation key.

Database

OpsMemory uses the shared Postgres instance with an opsmemory schema (OpsMemoryRuntimePersistenceExtensions.DefaultSchemaName = "opsmemory"). It is not EF Core; the schema is plain SQL embedded in __Libraries/StellaOps.OpsMemory/Migrations/001_initial_schema.sql and auto-migrated on startup via AddStartupMigrations<PostgresOptions>(...) (consistent with the repo-wide auto-migration mandate, not a manual init script). Tenant isolation is enforced at the query level (WHERE tenant_id = @tenantId), and the @tenantId the store receives is always the claim tenant — see the tenancy contract above; it is never taken from caller input. Similarity search uses array-stored vectors with in-store cosine similarity — no pgvector dependency.

Connection contract:

Dependencies