Sentinel-L7 is a multi-process Laravel application built to explore production patterns for async message processing, semantic caching, and fault-tolerant distributed systems. It processes any scored event stream β financial events, medical access logs, SaaS API activity, raw system telemetry β and classifies each event against an indexed corpus of domain-specific policy documents to determine whether it exceeds a risk threshold. A compliance engine (AML, GDPR, HIPAA) is the domain used here; the architecture is domain-agnostic.
%%{init: {'themeVariables': {'fontSize': '10px'}, 'flowchart': {'nodeSpacing': 15, 'rankSpacing': 30}}}%%
flowchart LR
subgraph Ingestion
A[Events<br/>XADD]
SL[synapse-l4]
end
subgraph SentinelL7["Sentinel-L7"]
subgraph Processing
B[Worker Pool<br/>PHP]
end
subgraph Intelligence
CASCADE[Cache β RAG β LLM β Fallback]
end
subgraph Interface
MCP[MCP Server]
E[React<br/>Dashboard]
end
subgraph Persistence
D[(Neon<br/>Postgres)]
end
end
AR[Arbiter-L8<br/>External Eval Harness]
subgraph DC["Downstream Consumers"]
direction TB
LE[Ledger-L5]
RL[Rhizome-Lens]
end
Interface ~~~ AR
Interface ~~~ DC
A -->|Redis Stream| B
SL -->|Validated Events| B
B -->|Evaluation Request| CASCADE
CASCADE -->|Verdict| D
D -->|Query| E
D -->|Usage Events| LE
SentinelL7 -->|OTel Traces/Logs| RL
AR -->|MCP: analyze-transaction<br/>driver override| MCP
AR -->|OTel Metrics/Traces| RL
click CASCADE "docs/diagrams/TRANSACTION_PIPELINE.md" "See the full transaction pipeline diagram"
click SL "https://github.com/obrienma/synapse-l4#readme" "Go to Synapse-L4 repo"
click AR "https://github.com/obrienma/Arbiter-L8#readme" "Go to Arbiter-L8 repo"
click LE "https://github.com/obrienma/Ledger-L5#readme" "Go to Ledger-L5 repo"
click RL "https://github.com/obrienma/Rhizome-Lens#readme" "Go to Rhizome-Lens repo"
class A,D teal
class B purple
class MCP,E amber
class SL,AR,LE,RL,CASCADE clickable
classDef clickable fill:#1d4ed8,stroke:#1e40af,stroke-width:2px,color:#ffffff
classDef teal fill:#E1F5EE,stroke:#0F6E56,color:#04342C
classDef purple fill:#EEEDFE,stroke:#534AB7,color:#26215C
classDef amber fill:#FDF0D5,stroke:#9A6B0A,color:#4A3200
style Ingestion fill:#D9D9D9,stroke:#5F5E5A,color:#000000
style SentinelL7 fill:#D9D9D9,stroke:#5F5E5A,color:#000000
style DC fill:#D9D9D9,stroke:#5F5E5A,color:#000000
style Processing fill:#FFFFFF,stroke:#5F5E5A,color:#000000
style Intelligence fill:#FFFFFF,stroke:#5F5E5A,color:#000000
style Interface fill:#FFFFFF,stroke:#5F5E5A,color:#000000
style Persistence fill:#FFFFFF,stroke:#5F5E5A,color:#000000
The "Cache β RAG β LLM β Fallback" box collapses the full cascade β see docs/diagrams/TRANSACTION_PIPELINE.md for the step-by-step breakdown, or the Pipeline Diagram under Architecture for the internal sequencing.
- π Contents
- π§° Stack
- π Running the Project
- ποΈ Architecture
- π Observability
- π Docs
- πΊοΈ Roadmap
π Backend & Ingestion
- Laravel 12 / PHP 8.4: Service container, queue, and Artisan command bus drive three long-running processes β web, transaction worker, and axiom worker β each consuming its own Redis Stream via
XREADGROUP. - Upstash Redis Streams:
XADD/XREADGROUP/XAUTOCLAIMprovide at-least-once delivery with a Pending Entry List; the web process never blocks waiting for analysis.
β‘ AI & Vector
- Ollama (default) + Gemini Flash + OpenRouter: LLM analysis runs through a swappable
ComplianceDriverinterface backed by a Laravel Service Manager; switching providers is a single env-var change, not a code change. Default is local/self-hostedqwen3.5:9b-q4_K_Mvia Ollama (ADR-0027) β no external API quota on the compliance-analysis path. - Upstash Vector: Named-namespace strategy (ADR-0026) β
transactions(semantic cache, β₯ 0.90 threshold β ADR-0015) cuts repeat LLM calls by 80%+;policies(RAG corpus, β₯ 0.70, domain-filtered) grounds compliance rulings in indexed regulatory documents (AML, HIPAA, GDPR). No data lives in Upstash's implicit default namespace.
ποΈ Frontend & Observability
- React 19 + Inertia.js + shadcn/ui: Server-driven SPA β no API layer needed; dashboard, compliance events, and CSV export all use Inertia page components with a dark-default shadcn/Tailwind v4 theme.
- OpenTelemetry: Wide spans on both worker processes export via OTLP to a companion Grafana stack (Tempo + Prometheus);
traceparentpropagated from Synapse-L4 stream entries continues the upstream trace as a child span (ADR-0024).
π§ͺ Testing & Infrastructure
- Pest: Feature, unit, and architecture tests; arch tests in
tests/ArchTest.phpenforce domain layer isolation βHttpandRedisfacade imports are banned insideApp\Services\Sentinel\Logic. - Neon PostgreSQL + Railway: Serverless Postgres (non-pooled host β
SELECT β¦ FOR UPDATE SKIP LOCKEDrequirement) forcompliance_eventsandtransactions; Railway hosts all three processes.
- PHP 8.4+ with Composer
- Node.js 20+
- Upstash account β Redis Streams + Vector namespaces (
transactions,policies) - Neon PostgreSQL database
- Gemini API key (or OpenRouter key if using that driver)
Note
Developed on WSL2 (Ubuntu) and deployed to Railway. Other environments may work but are untested.
# 1. Install dependencies
composer install && npm install
# 2. Copy env and fill in Upstash, Neon, and Gemini credentials
cp .env.example .env
# 3. Run migrations
php artisan migrate
# 4. Index policy documents into the vector knowledge base
php artisan sentinel:ingest
# 5. Start all processes (web + queue + logs + Vite + axiom watcher)
composer dev
# 6. In a separate terminal, generate transactions
php artisan sentinel:stream --limit=100
# 7. Open the dashboard
open http://localhost:8000/dashboard| Command | Description |
|---|---|
composer dev |
Start all five processes: web, queue, logs, Vite, axiom watcher |
php artisan sentinel:stream --limit=100 |
Simulate a transaction stream |
php artisan sentinel:watch |
Transaction worker (run alongside sentinel:stream for manual testing) |
php artisan sentinel:watch-axioms |
Axiom stream worker |
php artisan sentinel:ingest |
Index policy docs into vector KB (bumps policy epoch) |
php artisan sentinel:reset-metrics |
Reset dashboard counters |
php artisan sentinel:export-ground-truth --count=200 --output=ground-truth.json |
Export pre-AI labeled transactions (Arbiter-L8 offline eval ground truth) |
./vendor/bin/pest --filter=TestName |
Run a single test |
./vendor/bin/pint |
Run the Pint linter |
%%{init: {'themeVariables': {'fontSize': '10px'}, 'flowchart': {'nodeSpacing': 15, 'rankSpacing': 25}}}%%
flowchart LR
EH[EventHorizon]
XY[Xylem-L6]
AR[Arbiter-L8<br/>external eval harness]
EH ~~~ AR
subgraph Ingestion
A[Events<br/>XADD]
SL[synapse-l4]
end
subgraph SentinelL7["sentinel-l7"]
subgraph Processing
B[Worker Pool<br/>PHP]
end
subgraph Intelligence
CACHE[Semantic Cache]
LLM[Ollama LLM + RAG<br/>pluggable drivers]
FALLBACK[Rule-based Fallback]
end
subgraph Interface
E[React<br/>Dashboard]
end
subgraph Persistence
D[(Neon<br/>Postgres)]
end
end
subgraph "Downstream Consumers"
LE[Ledger-L5]
RL[Rhizome-Lens]
end
EH -->|Telemetry Events| SL
XY -->|SaaS Activity| SL
A -->|Redis Stream| B
SL -->|Validated Events| B
B -->|Evaluation Request| CACHE
CACHE -->|Cache Miss| LLM
LLM -->|Low Confidence| FALLBACK
B -->|Persist Results| D
D -->|Query| E
D -->|Usage Events| LE
SentinelL7 -->|OTel Traces/Logs| RL
AR -->|HTTP POST /ingest| SL
AR -->|MCP: analyze-transaction<br/>driver override| B
AR -->|OTel Metrics/Traces| RL
click EH "https://github.com/obrienma/EventHorizon#readme" "Go to EventHorizon repo"
click XY "https://github.com/obrienma/Xylem-L6#readme" "Go to Xylem-L6 repo"
click SL "https://github.com/obrienma/synapse-l4#readme" "Go to Synapse-L4 repo"
click AR "https://github.com/obrienma/Arbiter-L8#readme" "Go to Arbiter-L8 repo"
click LE "https://github.com/obrienma/Ledger-L5#readme" "Go to Ledger-L5 repo"
click RL "https://github.com/obrienma/Rhizome-Lens#readme" "Go to Rhizome-Lens repo"
classDef clickable fill:#1d4ed8,stroke:#1e40af,stroke-width:2px,color:#ffffff
class EH,XY,SL,AR,LE,RL clickable
The system is composed of three long-running processes plus a shared intelligence and persistence layer.
| Layer | Key Files | Purpose & Responsibilities |
|---|---|---|
| π Web | app/Http/Controllers/ Β· resources/js/Pages/ |
Dashboard & API: Inertia/React dashboard, compliance event pages, CSV export endpoint, HTTP rate-limited routes. |
| β‘ Transaction Worker | app/Console/Commands/WatchTransactions.php Β· app/Services/TransactionProcessorService.php |
Stream Consumer: XREADGROUP on transactions; semantic cache check β optional AI analysis β XACK. XAUTOCLAIM recovery pass at top of every loop iteration. |
| π· Axiom Worker | app/Console/Commands/WatchAxioms.php Β· app/Services/AxiomProcessorService.php |
Axiom Consumer: XREADGROUP on synapse:axioms; threshold routing (anomaly_score > 0.8) β AI audit narrative β Postgres. Every Axiom persisted β no silent drops. |
| π§ AI Layer | app/Contracts/ComplianceDriver.php Β· app/Services/ComplianceManager.php |
Driver Abstraction: Resolves ollama (default), gemini, or openrouter from env via Laravel Service Manager; domain logic only depends on the ComplianceDriver interface. |
| πΎ Vector Layer | app/Services/VectorCacheService.php Β· app/Services/EmbeddingService.php |
Semantic Cache + RAG: Upstash Vector transactions namespace (cache, β₯ 0.90) + policies namespace (RAG, β₯ 0.70, domain-filtered); fingerprint embedding via Ollama nomic-embed-text (768-dim) or Gemini embedding-001 (1536-dim), swappable via SENTINEL_EMBEDDING_DRIVER. |
| π MCP | app/Mcp/Servers/SentinelServer.php Β· routes/ai.php |
Agent Protocol: Model Context Protocol endpoint at POST /mcp; exposes analyze_transaction, search_policies, and get_recent_transactions tools. Synchronous JSON-RPC over HTTP β no queue, no durability. Consumed by interactive AI agents (Claude Desktop, Cursor, VS Code Copilot) and by Arbiter-L8, an external eval harness. See π MCP Server below for details. |
Note
Because both workers implement XAUTOCLAIM-based self-healing and the persistence layer uses idempotent writes (firstOrCreate + partial unique index on source_id), the worker pool can safely scale horizontally with zero risk of data duplication. Losing a worker doesn't stop recovery β any running sibling claims orphaned messages on its next loop iteration (ADR-0022). A delivery-count guard hard-ACKs poison messages at delivery_count >= 3 so a reliably crashing message cannot circulate indefinitely.
- Service Manager driver abstraction β
ComplianceManagerextends Laravel'sManager; swap AI providers viaSENTINEL_AI_DRIVERenv var, no code change required - Arch-test-enforced domain isolation β Pest architecture tests assert
App\Services\Sentinel\Logiccannot importHttporRedisfacades; enforced intests/ArchTest.php - Policy epoch invalidation β cached compliance verdicts carry an MD5 of the policy corpus; mismatched epochs on cache hits trigger re-analysis so no verdict survives a policy update unexamined
- Prompt versioning β all LLM templates live in
prompts/as versioned Markdown with changelogs andUsed by:lists; the activeComplianceDriverloads the compiled.txtform at runtime; prompt drift is visible in git like code drift - Named rate limiters β
RateLimiter::for()limiters on login, signup, and/dashboard/stream; all thresholds backed byRATE_LIMIT_*env vars viaconfig/sentinel.php
Mcp::web('/mcp', SentinelServer::class) (routes/ai.php) registers a plain synchronous HTTP route β the same request/response model as any Laravel controller, not a queued job. A caller's request is handled inline, in-process, and the compliance verdict is returned in that same HTTP response. Full tool request/response shapes are documented in API.md.
Tools:
analyze_transactionβ runs a transaction through the full pipeline (semantic cache β policy RAG β AI analysis, or Tier 3 fallback on infra failure)search_policiesβ semantic search over the indexed policy knowledge base (ns:policies, threshold β₯ 0.70)get_recent_transactionsβ reads the live Redis feed of recently processed transactions
analyze_transaction also accepts an optional driver (gemini/openrouter/ollama/vertexai) that forces a specific ComplianceManager provider instead of the configured default. Setting it bypasses the semantic vector cache entirely (no read, no write) and never falls back to Tier 3 on failure β a driver-override call always reflects that one provider's live verdict.
Note
Because the endpoint is a synchronous, non-durable RPC, a verdict only exists for the lifetime of the HTTP request that produced it β there's no queue or database row to recover it from if the caller drops the connection mid-call. This is a deliberate scope boundary, not an oversight: the durable, at-least-once pipeline (sentinel:watch on the transactions stream) is what production transactions go through. The MCP endpoint exists for callers that want an on-demand, synchronous verdict and can tolerate re-running a lost call β it was never meant to carry the same delivery guarantees as the stream pipeline.
Known consumers:
- Arbiter-L8 β an external Python evaluation harness. Its
adapters/sentinel_l7.pyspeaks MCP-over-HTTP directly to/mcpto score compliance verdicts against labeled ground truth (offline) and to run cross-provider disagreement checks via thedriveroverride (online) β see the "Per-requestComplianceManagerdriver override" entry under Shipped Features. - Interactive AI agents β Claude Desktop, Cursor, VS Code Copilot β call the tools ad hoc during a chat/coding session.
sequenceDiagram
autonumber
participant S as Redis Stream
participant W as Sentinel Worker
participant V as Upstash Vector
participant G as Gemini AI
S->>W: Fetch Transaction (XREADGROUP)
note over W,V: 2a. Semantic Cache Check (Namespace: transactions)
W->>V: Search Similar Results
alt Pattern Similarity β₯ 0.90
V-->>W: Return Cached Risk Report
Note over W: Bypasses LLM (Fast Path)
else Pattern New or Low Score
note over W,V: 2b. Policy Retrieval (Namespace: policies)
W->>V: Fetch Relevant Regulatory Rules
V-->>W: Return AML/HIPAA Context
W->>G: Analyze Intent + Policy Context
G-->>W: Policy-Grounded Risk Analysis
W->>V: Upsert New Vector + Metadata
end
W->>S: Acknowledge (XACK)
sequenceDiagram
autonumber
participant SL4 as Synapse-L4
participant AS as synapse:axioms
participant AW as Axiom Worker
participant V as Upstash Vector
participant G as Gemini AI
participant DB as Postgres
SL4->>AS: XADD (source_id, anomaly_score, status)
AS->>AW: XREADGROUP (axiom-workers)
alt anomaly_score > 0.8
note over AW,V: Policy Retrieval (Namespace: policies, β₯ 0.70)
AW->>V: Fetch Regulatory Context
V-->>AW: Policy Chunks
AW->>G: Analyze Axiom + Policy Context
G-->>AW: Audit Narrative + Risk Level
AW->>DB: firstOrCreate compliance_events (routed_to_ai=true)
else score β€ 0.8
AW->>DB: firstOrCreate compliance_events (routed_to_ai=false)
end
alt source_id not yet seen
DB-->>AW: row created
else re-delivery (same source_id)
DB-->>AW: existing row returned β no duplicate
end
AW->>AS: XACK (remove from PEL)
note over AW: Next loop iteration: XAUTOCLAIM
note over AW: any message idle > 30s β reassign + reprocess
stateDiagram-v2
[*] --> New: XADD to Stream
New --> Pending: Worker Reads (XREADGROUP)
state Pending {
[*] --> Processing
Processing --> Success: XACK (Done)
Processing --> Zombie: Worker Crashed
}
Zombie --> Processing: Sibling worker XAUTOCLAIM (min-idle > 30s)
Success --> [*]
classDiagram
direction TB
class ComplianceDriver {
<<interface>>
+analyze(array data) array
+analyzeTransaction(array data) array
}
class AbstractComplianceDriver {
<<abstract>>
#callModel(string prompt) string
+analyze(array data) array
+analyzeTransaction(array data) array
}
class OllamaDriver {
#callModel(string prompt) string
}
class GeminiDriver {
#callModel(string prompt) string
}
class OpenRouterDriver {
#callModel(string prompt) string
}
class ComplianceManager {
-Application app
+driver(string name) ComplianceDriver
#createOllamaDriver() ComplianceDriver
#createGeminiDriver() ComplianceDriver
#createOpenrouterDriver() ComplianceDriver
+getDefaultDriver() string
}
class ComplianceEngine {
-ComplianceDriver ai
+__construct(ComplianceDriver ai)
+process(array transaction) array
}
ComplianceDriver <|.. AbstractComplianceDriver : Realizes
AbstractComplianceDriver <|-- OllamaDriver : Extends
AbstractComplianceDriver <|-- GeminiDriver : Extends
AbstractComplianceDriver <|-- OpenRouterDriver : Extends
ComplianceManager ..> ComplianceDriver : Resolves
ComplianceEngine o-- ComplianceDriver : Injected
graph LR
subgraph "Protected Core"
Domain[App\Services\Sentinel\Logic]
end
subgraph "Infrastructure"
Http[Laravel Http Facade]
Redis[Redis Facade]
end
subgraph "Entry Points"
Web[App\Http\Controllers]
Console[App\Console\Commands]
end
Console --> Domain
Web --> Domain
Domain -.->|Forbidden| Http
Domain -.->|Forbidden| Redis
Domain -->|Allowed| Contract[ComplianceDriver Interface]
The application UI (/dashboard) is a server-driven React/Inertia SPA with four main surfaces:
- Live transaction feed β real-time stream of processed transactions with risk level, cache-hit/miss indicator, and AI routing signal
- Compliance Events β paginated audit trail of every persisted
compliance_eventsrow; toggle between flagged-only and all events; CSV export with optional date-range filter - Backpressure widget β consumer lag stat card reading
sentinel:consumer_lag(10s TTL), colour-coded emerald/amber/red against thelag_warn/lag_pausethresholds; shows a dash when the worker is offline - Metrics counters β processed count, cache hit rate, flagged event count; reset with
php artisan sentinel:reset-metrics
Note
Additional screenshots coming β the GIFs at the top of this README show the dashboard and terminal worker in action.
The Axiom Worker emits one axiom.process span per message β decorated with source_id, anomaly_score, domain, and routed_to_ai attributes β and continues the traceparent propagated from Synapse-L4, stitching the full cross-service trace. For deeper visibility, Sentinel exports to a companion Grafana monitoring stack (Tempo + Prometheus) that surfaces service health, processing latency p50/p95/p99, and AI routing signals that the HTTP response can't show.
The dashboard's AI Analysis by Driver and AI Confidence panels read ai.driver / ai.confidence attributes, which AxiomProcessorService::routeToAi() only sets when ComplianceDriver::analyze() succeeds. With a placeholder API key the call throws β the failure surfaces in the AI Errors panel and the two AI panels stay empty. To populate them:
- Set a working credential for the active
SENTINEL_AI_DRIVER:ollama(default) β reachableOLLAMA_URL+ pulledOLLAMA_CHAT_MODEL, no API key;openrouterβOPENROUTER_API_KEY(+OPENROUTER_MODEL);geminiβGEMINI_API_KEY(+GEMINI_FLASH_URL). - Send Axioms with
anomaly_score > AXIOM_AUDIT_THRESHOLD(default0.8) so they route to AI β sub-threshold Axioms never emitaxiom.ai_analysisattributes. - Run
php artisan sentinel:watch-axiomswith the OTel exporter pointed at the collector.
No dashboard change is needed once a driver call succeeds β the queries are already correct.
Tip
The dashboard lives in rhizome-observability. All 9 panels are TraceQL-metrics queries over axiom.process / axiom.ai_analysis span attributes β no Prometheus counters required. Requires Tempo β₯ 2.7 with filter_server_spans: false (Sentinel spans are INTERNAL-kind).
| File | Contents | Last updated |
|---|---|---|
| README.md | Project overview | 2026-07-17 |
| ARCHITECTURE.md | System design, data flows, worker loop structure | β |
| SERVICES.md | Per-service reference | β |
| API.md | HTTP + MCP routes | β |
| AI_PIPELINE.md | Gemini driver, RAG pipeline, prompt versioning | β |
| TESTING.md | Test strategy, known gaps | β |
| USER_STORIES.md | Compliance officer, platform engineer, AI agent | β |
| DEV_GETTING_STARTED.md | Full local setup walkthrough | β |
| journal.md | Engineering journal β one entry per phase | β |
| adr/ | Architecture Decision Records (ADR-0001 β ADR-0032) | β |
- OTel Phase 3 β EventHorizon instrumentation (four-stage RabbitMQ trace, malformed-message span events)
- EventHorizon deep-link β
source_idcorrelation from compliance event back to the originating EventHorizon event - Silent partial failure alerting β wire
under_indexedwarnings andquality_scorelogs to an active alert (e.g. N consecutive under-indexed queries on domain X, orquality_score=0for N consecutive events) - OAuth on the MCP endpoint β
Mcp::oauthRoutes()before production agent access - CI pipeline β architecture tests + unit suite running on every push
- End-to-end idempotency audit β verify EventHorizon event ID flows through Synapse-L4 as
source_idon the Axiom (early-exit dedup inAxiomProcessorServiceis done; source_id provenance through the full chain is not yet verified) - Fingerprint field reconciliation (ADR-0002/ADR-0015) β the transaction fingerprint now includes a randomly-templated
messagefield, adding entropy that may suppress cache hits; revisit alongside the open amount-representation (ADR-0002) and similarity-threshold (ADR-0015) questions - Ollama embedding threshold re-validation (ADR-0015/ADR-0025) β cutover is live (
SENTINEL_EMBEDDING_DRIVER=ollama, Upstash Vector index recreated at 768-dim,sentinel:ingestre-run against nomic-embed-text v1.5); still need to re-validateUPSTASH_VECTOR_THRESHOLDagainst nomic's score distribution before treatingollamaas the production default - Telemetry namespace β add a third named Upstash Vector namespace (e.g.
telemetry) following the pattern established in ADR-0026; no implicit/default namespace usage anywhere in the codebase - Tenant label passthrough on
compliance_events(ADR-0031, Proposed) β optionaltenantcolumn, sourced verbatim from Xylem-L6'stenantfield (Xylem-L6 ADR-0006) once its Synapse-L4 transmission wiring exists; gives Ledger-L5 a correlation key to joinrate_cards.customer_idagainst (Ledger-L5 ADR-0005). Narrow passthrough only β no auth/isolation changes, does not reopen ADR-0020.GET /usage's documentedcompliance_events[]shape (ADR-0029) already amended to include it ahead of the column existing; blocked on an addendum to Xylem-L6 ADR-0008 (addtenantto thePOST /ingestbody) and a new Synapse-L4 ADR (addtenanttoRawTelemetry), neither written yet - Policy corpus for SaaS API activity domain (ADR-0032, Accepted) β extends ADR-0018's existing single-tag domain filter with a
saas-domain policy corpus rather than a new mechanism. Corpus content is done βpolicies/saas-mitre-attack-alignment.md(SAAS-MITRE-001),policies/saas-nist-authentication-alignment.md(SAAS-NIST-001),policies/saas-owasp-api-security-alignment.md(SAAS-OWASP-001), all taggeddomain = 'saas'at ingest β and the single-tag-vs-OR-filter question is resolved as single-tag (the three documents are complementary lenses on the same signals, not disjoint frameworks; see ADR-0032). What's left:WatchAxioms/the Synapse-L4 emitter still needs to stampdomainon real SaaS-sourced Axiom payloads before this filter activates outside tests (AxiomProcessorServiceitself already reads/persists/forwardsdomainwhen present β this is the CLAUDE.md-tracked "domain activation" gap, one level upstream of that). Prerequisite named by Xylem-L6 ADR-0004. - WorkOS AuthKit multi-tenant auth (ADR-0033, Accepted, reverses ADR-0020) β replaces
AuthController's hand-rolledAuth::attempt()flow with WorkOS AuthKit; a WorkOS Organization maps to a Sentinel-L7 tenant via a newtenant_idcolumn onUser, resolving the two outstanding TODOs (routes/web.phptenant-scoping comment andDashboardController's deferredtenant_idscoping). SSO/Directory Sync/Audit Log export are explicitly out of scope for this phase β AuthKit alone establishes the tenant boundary.rhizo-bookis untouched. Not yet implemented.
- Semantic cache can permanently amplify a single wrong verdict for narrow-profile merchants. The Upstash Vector cache (similarity threshold 0.90) matches on embedding similarity, not transaction identity. A merchant profile whose transactions are narrow enough in amount range and message wording (e.g. the
suspicious-category simulation profile) can embed near-identically across every transaction it generates β so if the first one is ever misanalyzed, every subsequent similar transaction inherits that one stale, wrong cached verdict indefinitely, rather than getting an independent re-analysis. Discovered during Arbiter-L8's Phase 3 step 8 live judge validation (worked around there via the per-request driver override, which bypasses the cache entirely β see Arbiter-L8'sdocs/journal/arbiter-l8-2026-07-04T1720-ground-truth-export-and-judge-validation.md). Not yet fixed here; no cache-invalidation or per-merchant TTL exists today.
Tip
22+ features shipped | Pest architecture + unit suite green
π View shipped features...
- Core pipeline β Redis Streams, semantic cache, fault tolerance (XCLAIM)
- Backpressure step 2 β XREADGROUP + XAUTOCLAIM self-healing worker pool (ADR-0022): transaction stream migrated to consumer group
sentinel-consumers;XAUTOCLAIMembedded at top of each worker loop; dead-letter guard ACKs poison messages atdelivery_count >= 3; dedicated reclaimer daemon removed - Backpressure step 3 β graduated consumer lag signal (ADR-0023): worker writes
XPENDINGcount tosentinel:consumer_lag(TTL 10s); producer applies soft-limit sleep (500ms, configurable) at lag > 50, spin-wait at lag > 200 - Weighted transaction simulation β
simulation.merchantsconfig holds weighted profiles (category, weight, amount range, currencies,is_threat) instead of a flat uniform-probability list;TransactionStreamService::generate()samples via an index-repetition pool so traffic mix reflects realistic merchant volume - Benchmark seeder β
database/seeders/TransactionSeeder.phpruns N simulated transactions through the live pipeline and reports cache hit rate, fallbacks, embedding API call count, and threat rate
- ComplianceDriver stack β
OllamaDriver(qwen3.5:9b-q4_K_M, default β ADR-0027),GeminiDriver(Gemini Flash),OpenRouterDriver(OpenAI-compatible),VertexAIDriver(Claude Sonnet 4.6 via Vertex AI/Agent Platform, service account + IAM auth β ADR-0030), all sharing policy RAG/quality-scoring/response-parsing viaAbstractComplianceDriver; swap via env,ComplianceManager(Laravel Service Manager pattern) VertexAIDriver(ADR-0030, amended) β fourth compliance driver,SENTINEL_AI_DRIVER=vertexai, calls Claude Sonnet 4.6 viapublishers/anthropic/models/claude-sonnet-4-6:rawPredict(Anthropic Messages API shape, not Gemini'sgenerateContent); auth viagoogle/auth'sServiceAccountCredentials(roles/aiplatform.user) minting a per-call OAuth2 bearer token, wrapped in a smallVertexAiTokenServicesoVertexAIDriverTestcan mock the token boundary instead of hitting Google's real OAuth2 endpoint;'vertexai'added toAnalyzeTransaction's MCP toolDRIVERSallowlist per the ADR. No free tier β billed per-token at direct-API rates; every request explicitly setsthinking: disabled+effort: lowsince Sonnet 4.6's default (high-effort adaptive thinking) is expensive overkill for this driver's short JSON-classification workload- Ollama as default compliance-analysis driver (ADR-0027) β
SENTINEL_AI_DRIVERdefaults toollama; verified live against the real host (raw JSON-mode call, and a fullTransactionProcessorServicecache-miss call producing a correctly-flagged critical-risk verdict with real policy citations).think: falseavoids a ~20x latency penalty fromqwen3.5's reasoning trace; Gemini/OpenRouter remain available via env override, no removal - Policy RAG β
sentinel:ingestchunking pipeline,policies/corpus, score-aware query formulation - Domain-scoped RAG retrieval β
domainmetadata tag at ingest; server-side filter at query time; retrieval quality logging - Output quality scoring β 4-signal rubric on every compliance driver response;
low quality scorewarning when score β€ 1 policy_refsfabrication guard (ADR-0034) βAbstractComplianceDriverforcespolicy_refs = []whenever the retrieval that produced the response hadchunk_count === 0, regardless of what the model returned; apolicy_refs fabricated on empty retrievalwarning logs the correction so the fabrication rate stays observable. Live sample data (49cache_misscalls,openrouter) showed the model asserting non-emptypolicy_refson 18/49 zero-chunk retrievals at avg. confidence 0.88. ADR-0019'shas_policy_refsrubric signal now inherits the corrected value automatically. Scope: only the structuredpolicy_refsfield is guarded β a citation embedded in free-textnarrative(the field actually persisted tocompliance_events.audit_narrative) is untouched, and correspondence between a cited ref and an actually-retrieved chunk isn't checked whenchunk_count > 0; both are open TODOs.- Retrieval coverage logging β
mean_scoreandunder_indexedper RAG query;Log::warningfires when a domain filter returns < 2 chunks - EmbeddingDriver stack (ADR-0025) β
GeminiEmbeddingDriver,OllamaEmbeddingDriver(nomic-embed-text v1.5, 768-dim, task-prefixedsearch_document/search_queryinputs),EmbeddingManager(Service Manager pattern), swap viaSENTINEL_EMBEDDING_DRIVER;EmbeddingServicenow delegates to the resolved driver instead of calling Gemini directly. Live in this environment: Upstash Vector index recreated at 768-dim, policy KB re-ingested against Ollama. - Named Vector namespaces (ADR-0026) β
VectorCacheServiceno longer has any bare/default-namespace methods; transaction semantic cache moved from Upstash's implicit default namespace to an explicittransactionsnamespace, matchingpolicies. Sets the pattern for future namespaces (e.g. telemetry) and tenant-prefixed namespacing. - ADR-0007 Tier 2 drift closed β
TransactionProcessorServicenow callsComplianceDriver::analyzeTransaction()(Gemini/OpenRouter + policy RAG) on a cache miss instead of the rule-basedThreatAnalysisService;ThreatAnalysisServiceis reserved for Tier 3 (infra failure) as ADR-0007 originally specified. Newtransaction-compliance-analysisprompt added for the transaction-shaped query. - Per-request
ComplianceManagerdriver override βTransactionProcessorService::process()and theanalyze_transactionMCP tool accept an optionaldriver(gemini/openrouter/ollama/vertexai) that bypasses the semantic vector cache entirely (no read, no write) and never falls back to Tier 3 on failure, so the same transaction can be scored through two different providers for cross-provider disagreement measurement. Built for Arbiter-L8's online disagreement layer.
- Synapse-L4 Axiom ingestion β
synapse:axiomsRedis stream +sentinel:watch-axiomsworker - Synapse-L4 Python sidecar β FastAPI LLM judge pass + Redis emitter
- Idempotent Axiom persistence β
firstOrCreate+ partial unique index onsource_id+UniqueConstraintViolationExceptioncatch; re-delivered stream messages never produce duplicatecompliance_eventsrows - Early-exit idempotency in
AxiomProcessorServiceβEXISTScheck onsource_idbefore AI routing; duplicate re-deliveries short-circuit before Gemini is called; DB-layerfirstOrCreateremains as concurrent-race fallback - XCLAIM recovery for
synapse:axiomsconsumer group βXAUTOCLAIMembedded in worker loop (ADR-0022) - Rule-based Tier 3 fallback for Axioms β
AxiomThreatAnalysisServicegivesAxiomProcessorServicean ADR-0007-style deterministic verdict (risk_level: high, threshold-referencing narrative) when Gemini/OpenRouter throws, instead of persistingrisk_level: unknown/narrative: null;driver_usedis stampedfallbackso the degraded path is observable
compliance_eventsaudit trail β Postgres persistence withsource_idcorrelation- Transaction history β processed transactions persisted to Postgres
transactionstable - Compliance dashboard β Flags / Events nav pages surfacing
compliance_events - Compliance report CSV export β
GET /compliance/exportstreams flagged/all events chunked at 500 rows; optionalfrom/todate filters; UI date-range picker on the Compliance page - Transaction-pipeline idempotency guard β partial unique index on
transactions.txn_id(excludesdriver_override, which intentionally writes multiple rows per transaction) plus an early-exit dedup check inTransactionProcessorService::process(), closing anXAUTOCLAIM-redelivery double-billing risk found while drafting ADR-0028; mirrors the Axiom pipeline's existingsource_iddedup pattern - Billing classification (ADR-0028, Accepted) β defines which
transactions.source/compliance_events.driver_usedrows Ledger-L5's usage-pull query should treat as billable vs. cache-savings vs. excluded; no sentinel-l7 instrumentation beyond the idempotency guard above, which was a prerequisite for the classification to be safe to rely on GET /usageendpoint (ADR-0029) β dual per-pipeline cursor pull (since_transactions/since_compliance_events, both auto-incrementid) with a config-backed page size and safety-lag window;X-Ledger-Api-Key-gated (VerifyLedgerApiKeymiddleware), HTTPS-enforced outside local/testing, 401 on missing/invalid key
- React 19 + shadcn/ui dashboard with live transaction feed
- Backpressure dashboard widget β consumer lag stat card reads
sentinel:consumer_lag(10s TTL); colour-coded emerald/amber/red againstlag_warn/lag_pauseconfig thresholds; dash when worker is offline - HTTP rate limiting β named
RateLimiter::for()limiters on login (5/min per IP), signup (10/hr per IP),/dashboard/stream(20/min per authenticated user); all thresholds config-backed viaRATE_LIMIT_*env vars
- MCP server β exposes
analyze_transaction,search_policies,get_recent_transactionstools to AI agents via Model Context Protocol atPOST /mcp - OTel instrumentation (Phase 2) β
OtelServiceProviderbootstraps SDK (BatchSpanProcessor β OTLP HTTP);AxiomProcessorServicewraps processing in wide spans withsource_id,anomaly_score,domain,routed_to_aiattributes;traceparentextracted from stream entries to continue Synapse-L4 trace as child span (ADR-0024) - Grafana dashboard β "Sentinel-L7 Service" β 9 panels, TraceQL-metrics queries over
axiom.process/axiom.ai_analysisspans (no Prometheus counters required); throughput by risk level/domain/AI routing, latency p50/p95/p99, anomaly-score and AI-confidence aggregates



