Upload folder using huggingface_hub

#60
Dockerfile CHANGED
@@ -1,13 +1,7 @@
1
  FROM python:3.12-slim
2
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
3
  WORKDIR /app
4
-
5
  COPY requirements.txt .
6
-
7
- # Use the secret ARF_GITHUB_PAT during the pip install step
8
- RUN --mount=type=secret,id=ARF_GITHUB_PAT \
9
- git config --global url."https://x-access-token:$(cat /run/secrets/ARF_GITHUB_PAT)@github.com/".insteadOf "https://github.com/" && \
10
- pip install --no-cache-dir -r requirements.txt
11
-
12
  COPY . .
13
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  FROM python:3.12-slim
2
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
3
  WORKDIR /app
 
4
  COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
6
  COPY . .
7
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,9 +1,3 @@
1
- ---
2
- title: ARF API Control Plane
3
- sdk: docker
4
- colorFrom: blue
5
- colorTo: green
6
- ---
7
  # arf-api
8
 
9
  ARF API Control Plane (FastAPI)
@@ -87,7 +81,7 @@ curl -X POST "http://localhost:8000/api/v1/v1/incidents/evaluate" -H "Content-
87
  "justification": "Causal: If we apply restart_container instead of no_action, latency would change from 600.00 to 510.00 (Δ = -90.00). Based on heuristic causal model.",
88
  "confidence": 0.85,
89
  "risk_score": 0.54,
90
- "status": "oss_advisory_only"
91
  },
92
  "causal_explanation": {
93
  "factual_outcome": 600,
@@ -123,4 +117,5 @@ Notes
123
  -----
124
 
125
  - The governance endpoints use an in-process `RiskEngine` initialized at startup.
126
- - The outcome recording endpoint is not implemented in this repository and returns HTTP 501.
 
 
 
 
 
 
 
 
1
  # arf-api
2
 
3
  ARF API Control Plane (FastAPI)
 
81
  "justification": "Causal: If we apply restart_container instead of no_action, latency would change from 600.00 to 510.00 (Δ = -90.00). Based on heuristic causal model.",
82
  "confidence": 0.85,
83
  "risk_score": 0.54,
84
+ "status": "success"
85
  },
86
  "causal_explanation": {
87
  "factual_outcome": 600,
 
117
  -----
118
 
119
  - The governance endpoints use an in-process `RiskEngine` initialized at startup.
120
+ - The outcome recording endpoint is not implemented in this repository and returns HTTP 501.
121
+
app/api/deps.py CHANGED
@@ -1,20 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import sys
2
  from app.database.session import SessionLocal
3
  from slowapi import Limiter
4
  from slowapi.util import get_remote_address
5
  from app.core.config import settings
6
 
 
 
7
  # ARF core engine imports
8
  from agentic_reliability_framework.core.governance.risk_engine import RiskEngine
9
  from agentic_reliability_framework.core.decision.decision_engine import DecisionEngine
10
  from agentic_reliability_framework.core.governance.stability_controller import LyapunovStabilityController
11
- from agentic_reliability_framework.core.governance.causal_explainer import CausalExplainer
12
  from agentic_reliability_framework.runtime.memory.rag_graph import RAGGraphMemory
13
  from agentic_reliability_framework.core.models.event import ReliabilityEvent, HealingAction
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- # Dependency to get DB session
17
  def get_db():
 
 
 
 
 
18
  db = SessionLocal()
19
  try:
20
  yield db
@@ -22,23 +53,75 @@ def get_db():
22
  db.close()
23
 
24
 
25
- # Rate limiter with default limit from settings
 
 
 
26
  limiter = Limiter(
27
  key_func=get_remote_address,
28
- default_limits=[
29
- settings.RATE_LIMIT])
 
 
 
 
 
 
 
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # ARF engine dependencies (singletons for simplicity)
33
  _risk_engine = None
34
  _decision_engine = None
35
  _stability_controller = None
36
  _causal_explainer = None
37
  _rag_graph = None
 
 
38
 
 
 
 
 
39
 
40
- def _seed_rag_graph(rag):
41
- """Seed the RAG graph with historical healing action outcomes."""
 
 
 
42
  seed_data = [
43
  ("seed_restart_1", "test", HealingAction.RESTART_CONTAINER.value, True, 2),
44
  ("seed_restart_2", "test", HealingAction.RESTART_CONTAINER.value, True, 3),
@@ -58,19 +141,23 @@ def _seed_rag_graph(rag):
58
  component=comp,
59
  latency_p99=500,
60
  error_rate=0.1,
61
- service_mesh="default"
62
  )
63
  rag.record_outcome(
64
  incident_id=inc_id,
65
  event=event,
66
  action_taken=action,
67
  success=success,
68
- resolution_time_minutes=res_time
69
  )
70
  print("Seeded RAG graph with historical data", file=sys.stderr)
71
 
72
 
73
- def get_rag_graph():
 
 
 
 
74
  global _rag_graph
75
  if _rag_graph is None:
76
  _rag_graph = RAGGraphMemory()
@@ -78,7 +165,11 @@ def get_rag_graph():
78
  return _rag_graph
79
 
80
 
81
- def get_decision_engine():
 
 
 
 
82
  global _decision_engine
83
  if _decision_engine is None:
84
  rag = get_rag_graph()
@@ -86,22 +177,51 @@ def get_decision_engine():
86
  return _decision_engine
87
 
88
 
89
- def get_risk_engine():
 
 
 
90
  global _risk_engine
91
  if _risk_engine is None:
92
  _risk_engine = RiskEngine()
93
  return _risk_engine
94
 
95
 
96
- def get_stability_controller():
 
 
 
97
  global _stability_controller
98
  if _stability_controller is None:
99
  _stability_controller = LyapunovStabilityController()
100
  return _stability_controller
101
 
102
 
103
- def get_causal_explainer():
 
 
 
 
 
 
104
  global _causal_explainer
105
  if _causal_explainer is None:
106
- _causal_explainer = CausalExplainer()
107
  return _causal_explainer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dependency injection module for the ARF Agentic Reliability Framework API.
3
+
4
+ Provides FastAPI dependencies for database sessions, rate limiting, and
5
+ singleton instances of the core ARF engines (RiskEngine, DecisionEngine,
6
+ LyapunovStabilityController, CausalEffectEstimator, RAGGraphMemory, and
7
+ (v4.3.1) SkillRegistry). All engine dependencies are lazily initialised
8
+ and cached for the lifetime of the application process.
9
+
10
+ v4.3.2: Added verify_internal_key dependency to secure direct API access.
11
+ """
12
+
13
+ import os
14
  import sys
15
  from app.database.session import SessionLocal
16
  from slowapi import Limiter
17
  from slowapi.util import get_remote_address
18
  from app.core.config import settings
19
 
20
+ from fastapi import Header, HTTPException, Request
21
+
22
  # ARF core engine imports
23
  from agentic_reliability_framework.core.governance.risk_engine import RiskEngine
24
  from agentic_reliability_framework.core.decision.decision_engine import DecisionEngine
25
  from agentic_reliability_framework.core.governance.stability_controller import LyapunovStabilityController
26
+ from agentic_reliability_framework.core.governance.causal_effect_estimator import CausalEffectEstimator
27
  from agentic_reliability_framework.runtime.memory.rag_graph import RAGGraphMemory
28
  from agentic_reliability_framework.core.models.event import ReliabilityEvent, HealingAction
29
 
30
+ # ── v4.3.1: Skill Registry (optional) ──────────────────────────
31
+ try:
32
+ from agentic_reliability_framework.core.governance.skill_registry import SkillRegistry
33
+ _SKILL_REGISTRY_AVAILABLE = True
34
+ except ImportError:
35
+ SkillRegistry = None
36
+ _SKILL_REGISTRY_AVAILABLE = False
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Database dependency
41
+ # ---------------------------------------------------------------------------
42
 
 
43
  def get_db():
44
+ """
45
+ Yield a SQLAlchemy database session and ensure it is closed after use.
46
+
47
+ This dependency is intended to be used with FastAPI's `Depends` mechanism.
48
+ """
49
  db = SessionLocal()
50
  try:
51
  yield db
 
53
  db.close()
54
 
55
 
56
+ # ---------------------------------------------------------------------------
57
+ # Rate limiter
58
+ # ---------------------------------------------------------------------------
59
+
60
  limiter = Limiter(
61
  key_func=get_remote_address,
62
+ default_limits=[settings.RATE_LIMIT],
63
+ )
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Internal API key verification (v4.3.2)
68
+ # ---------------------------------------------------------------------------
69
+
70
+ INTERNAL_API_KEY = os.getenv("ARF_INTERNAL_API_KEY", "")
71
 
72
+ async def verify_internal_key(x_internal_key: str = Header(default=None, alias="X-Internal-Key")):
73
+ """
74
+ FastAPI dependency that verifies the internal API key header.
75
+
76
+ If the environment variable ARF_INTERNAL_API_KEY is set, the request
77
+ must include a matching X‑Internal‑Key header. Without the key, a
78
+ 401 Unauthorized response is returned.
79
+
80
+ This guards against direct access to the API when deployed behind
81
+ the Go gateway. The gateway is configured to inject this header
82
+ for authenticated requests.
83
+ """
84
+ if INTERNAL_API_KEY:
85
+ if x_internal_key is None:
86
+ raise HTTPException(status_code=401, detail="Missing internal API key")
87
+ # Use a constant‑time comparison to avoid timing attacks.
88
+ if not _constant_time_compare(x_internal_key, INTERNAL_API_KEY):
89
+ raise HTTPException(status_code=401, detail="Invalid internal API key")
90
+ # If no key is configured, the dependency is a no‑op (for local dev).
91
+
92
+
93
+ def _constant_time_compare(a: str, b: str) -> bool:
94
+ """Compare two strings in constant time to prevent timing attacks."""
95
+ if len(a) != len(b):
96
+ return False
97
+ result = 0
98
+ for x, y in zip(a, b):
99
+ result |= ord(x) ^ ord(y)
100
+ return result == 0
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Singleton engine instances (lazy, cached)
105
+ # ---------------------------------------------------------------------------
106
 
 
107
  _risk_engine = None
108
  _decision_engine = None
109
  _stability_controller = None
110
  _causal_explainer = None
111
  _rag_graph = None
112
+ _skill_registry = None
113
+
114
 
115
+ def _seed_rag_graph(rag: RAGGraphMemory) -> None:
116
+ """
117
+ Populate the RAG graph with a small set of synthetic historical
118
+ healing‑action outcomes to provide initial memory for the decision engine.
119
 
120
+ Parameters
121
+ ----------
122
+ rag : RAGGraphMemory
123
+ An already‑instantiated RAG graph memory instance.
124
+ """
125
  seed_data = [
126
  ("seed_restart_1", "test", HealingAction.RESTART_CONTAINER.value, True, 2),
127
  ("seed_restart_2", "test", HealingAction.RESTART_CONTAINER.value, True, 3),
 
141
  component=comp,
142
  latency_p99=500,
143
  error_rate=0.1,
144
+ service_mesh="default",
145
  )
146
  rag.record_outcome(
147
  incident_id=inc_id,
148
  event=event,
149
  action_taken=action,
150
  success=success,
151
+ resolution_time_minutes=res_time,
152
  )
153
  print("Seeded RAG graph with historical data", file=sys.stderr)
154
 
155
 
156
+ def get_rag_graph() -> RAGGraphMemory:
157
+ """
158
+ Return a singleton instance of the RAG graph memory, seeded with
159
+ synthetic historical data on first access.
160
+ """
161
  global _rag_graph
162
  if _rag_graph is None:
163
  _rag_graph = RAGGraphMemory()
 
165
  return _rag_graph
166
 
167
 
168
+ def get_decision_engine() -> DecisionEngine:
169
+ """
170
+ Return a singleton DecisionEngine, wiring it to the shared RAG graph
171
+ memory.
172
+ """
173
  global _decision_engine
174
  if _decision_engine is None:
175
  rag = get_rag_graph()
 
177
  return _decision_engine
178
 
179
 
180
+ def get_risk_engine() -> RiskEngine:
181
+ """
182
+ Return a singleton RiskEngine instance.
183
+ """
184
  global _risk_engine
185
  if _risk_engine is None:
186
  _risk_engine = RiskEngine()
187
  return _risk_engine
188
 
189
 
190
+ def get_stability_controller() -> LyapunovStabilityController:
191
+ """
192
+ Return a singleton LyapunovStabilityController instance.
193
+ """
194
  global _stability_controller
195
  if _stability_controller is None:
196
  _stability_controller = LyapunovStabilityController()
197
  return _stability_controller
198
 
199
 
200
+ def get_causal_explainer() -> CausalEffectEstimator:
201
+ """
202
+ Return a singleton CausalEffectEstimator instance.
203
+
204
+ The estimator uses Inverse Probability Weighting (IPW) and causal forests
205
+ to provide counterfactual explanations for governance decisions.
206
+ """
207
  global _causal_explainer
208
  if _causal_explainer is None:
209
+ _causal_explainer = CausalEffectEstimator()
210
  return _causal_explainer
211
+
212
+
213
+ def get_skill_registry() -> "Optional[SkillRegistry]":
214
+ """
215
+ Return a singleton SkillRegistry instance (v4.3.1).
216
+
217
+ The registry manages procedural skill artefacts, versioning, per‑skill
218
+ reliability models (Beta‑Binomial), and the COLLECT‑DIAGNOSE‑REVISE‑PROMOTE
219
+ evolution loop. If the SkillRegistry module is not installed, returns None.
220
+ """
221
+ global _skill_registry
222
+ if not _SKILL_REGISTRY_AVAILABLE:
223
+ return None
224
+ if _skill_registry is None:
225
+ from agentic_reliability_framework.core.governance.skill_registry import SkillRegistry
226
+ _skill_registry = SkillRegistry()
227
+ return _skill_registry
app/api/routes_governance.py CHANGED
@@ -1,25 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks, Header
2
  from fastapi.encoders import jsonable_encoder
3
  from sqlalchemy.orm import Session
4
- from app.models.infrastructure_intents import InfrastructureIntentRequest
5
- from app.services.intent_adapter import to_oss_intent
6
- from app.services.risk_service import evaluate_intent, evaluate_healing_decision
7
- from app.services.intent_store import save_evaluated_intent
8
- from app.services.outcome_service import record_outcome
9
- from app.api.deps import get_db
10
  from pydantic import BaseModel
11
  import uuid
12
  import logging
13
  import time
14
- from typing import Optional
 
15
 
 
 
 
 
 
 
 
16
  from agentic_reliability_framework.core.models.event import ReliabilityEvent
 
 
 
 
 
17
 
18
- # ===== USAGE TRACKER IMPORTS =====
19
  import app.core.usage_tracker
20
  from app.core.usage_tracker import UsageRecord
21
 
22
- # ===== PRICING CALCULATOR INTEGRATION =====
23
  try:
24
  from arf_pricing_calculator.storage.buffer import add_event
25
  PRICING_AVAILABLE = True
@@ -27,7 +57,15 @@ except ImportError:
27
  PRICING_AVAILABLE = False
28
  add_event = None
29
 
30
- # ===== OpenTelemetry (optional) =====
 
 
 
 
 
 
 
 
31
  try:
32
  from opentelemetry import trace
33
  from opentelemetry.trace import Status, StatusCode
@@ -38,7 +76,9 @@ except ImportError:
38
  _tracer = None
39
 
40
  logger = logging.getLogger(__name__)
41
- router = APIRouter()
 
 
42
 
43
 
44
  class OutcomeRequest(BaseModel):
@@ -46,12 +86,107 @@ class OutcomeRequest(BaseModel):
46
  success: bool
47
  recorded_by: str
48
  notes: str = ""
 
 
 
49
 
50
 
51
  class HealingDecisionRequest(BaseModel):
52
  event: ReliabilityEvent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  @router.post("/intents/evaluate")
56
  async def evaluate_intent_endpoint(
57
  request: Request,
@@ -59,11 +194,13 @@ async def evaluate_intent_endpoint(
59
  background_tasks: BackgroundTasks,
60
  db: Session = Depends(get_db),
61
  idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
 
62
  ):
63
  """
64
- Evaluate an infrastructure intent with idempotency and atomic quota consumption.
 
 
65
  """
66
- # ── optional trace ──────────────────────────────────────
67
  span = None
68
  if OTEL_AVAILABLE and _tracer:
69
  span = _tracer.start_span("governance.evaluate_intent")
@@ -75,13 +212,22 @@ async def evaluate_intent_endpoint(
75
  if not api_key:
76
  api_key = request.query_params.get("api_key", "unknown")
77
 
 
 
 
 
 
 
 
 
 
 
78
  current_tracker = app.core.usage_tracker.tracker
79
  if current_tracker is None:
80
  if span:
81
  span.set_status(Status(StatusCode.ERROR, "tracker unavailable"))
82
  span.end()
83
- raise HTTPException(status_code=503,
84
- detail="Usage tracking service unavailable")
85
 
86
  record = UsageRecord(
87
  api_key=api_key,
@@ -102,40 +248,85 @@ async def evaluate_intent_endpoint(
102
  if existing_response:
103
  return existing_response
104
  else:
105
- raise HTTPException(status_code=429,
106
- detail="Monthly evaluation quota exceeded")
107
 
108
  try:
109
  oss_intent = to_oss_intent(intent_req)
110
  risk_engine = request.app.state.risk_engine
111
- result = evaluate_intent(
112
- engine=risk_engine,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  intent=oss_intent,
114
- cost_estimate=intent_req.estimated_cost,
115
- policy_violations=intent_req.policy_violations
 
 
 
 
 
 
 
 
 
 
116
  )
117
 
118
  if span:
119
  span.set_attribute("risk_score", result["risk_score"])
120
- span.set_attribute("deterministic_id", str(uuid.uuid4())) # will be overwritten later, but fine for trace
121
 
122
- deterministic_id = str(uuid.uuid4())
123
  api_payload = jsonable_encoder(intent_req.model_dump())
124
  oss_payload = jsonable_encoder(oss_intent.model_dump())
125
 
126
  save_evaluated_intent(
127
  db=db,
128
  deterministic_id=deterministic_id,
 
129
  intent_type=intent_req.intent_type,
130
  api_payload=api_payload,
131
  oss_payload=oss_payload,
132
  environment=str(intent_req.environment),
133
- risk_score=result["risk_score"]
134
  )
135
 
136
  result["intent_id"] = deterministic_id
137
  response_data = result
138
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  if current_tracker:
140
  background_tasks.add_task(
141
  current_tracker._insert_audit_log,
@@ -172,18 +363,18 @@ async def evaluate_intent_endpoint(
172
  raise HTTPException(status_code=500, detail=error_msg)
173
 
174
 
 
 
 
175
  @router.post("/intents/outcome")
176
  async def record_outcome_endpoint(
177
  request: Request,
178
  outcome: OutcomeRequest,
179
  db: Session = Depends(get_db),
180
  idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
 
181
  ):
182
- """
183
- Record an outcome for a previously evaluated intent.
184
- Idempotent based on deterministic_id and success value (handled in service).
185
- Also updates the pricing calculator's calibration buffer if available.
186
- """
187
  try:
188
  risk_engine = request.app.state.risk_engine
189
  outcome_record = record_outcome(
@@ -194,6 +385,9 @@ async def record_outcome_endpoint(
194
  notes=outcome.notes,
195
  risk_engine=risk_engine,
196
  idempotency_key=idempotency_key,
 
 
 
197
  )
198
 
199
  if PRICING_AVAILABLE and add_event is not None:
@@ -205,30 +399,31 @@ async def record_outcome_endpoint(
205
  "source": "arf_api_outcome"
206
  }
207
  add_event(event)
208
- logger.info(
209
- f"Added outcome to pricing buffer for intent {
210
- outcome.deterministic_id}")
211
  except Exception as e:
212
- logger.warning(
213
- f"Failed to update pricing buffer for intent {
214
- outcome.deterministic_id}: {e}")
215
 
216
  return {"message": "Outcome recorded", "outcome_id": outcome_record.id}
217
  except Exception as e:
218
  raise HTTPException(status_code=500, detail=str(e))
219
 
220
 
 
 
 
221
  @router.post("/healing/evaluate")
222
  async def evaluate_healing_decision_endpoint(
223
  request: Request,
224
  decision_req: HealingDecisionRequest,
225
  background_tasks: BackgroundTasks,
 
226
  idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
 
227
  ):
228
  """
229
- Evaluate a healing decision with idempotency and atomic quota consumption.
 
230
  """
231
- # ── optional trace ──────────────────────────────────────
232
  span = None
233
  if OTEL_AVAILABLE and _tracer:
234
  span = _tracer.start_span("governance.evaluate_healing")
@@ -239,13 +434,21 @@ async def evaluate_healing_decision_endpoint(
239
  if not api_key:
240
  api_key = request.query_params.get("api_key", "unknown")
241
 
 
 
 
 
 
 
 
 
 
242
  current_tracker = app.core.usage_tracker.tracker
243
  if current_tracker is None:
244
  if span:
245
  span.set_status(Status(StatusCode.ERROR, "tracker unavailable"))
246
  span.end()
247
- raise HTTPException(status_code=503,
248
- detail="Usage tracking service unavailable")
249
 
250
  record = UsageRecord(
251
  api_key=api_key,
@@ -266,8 +469,7 @@ async def evaluate_healing_decision_endpoint(
266
  if existing_response:
267
  return existing_response
268
  else:
269
- raise HTTPException(status_code=429,
270
- detail="Monthly evaluation quota exceeded")
271
 
272
  try:
273
  policy_engine = request.app.state.policy_engine
@@ -282,6 +484,39 @@ async def evaluate_healing_decision_endpoint(
282
  rag_graph=rag_graph,
283
  model=model,
284
  tokenizer=tokenizer,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  )
286
 
287
  if span:
 
1
+ """
2
+ Routes for governance evaluation – tenant‑aware, audited, and Rust‑enforced.
3
+
4
+ This module provides the primary API endpoints for evaluating infrastructure
5
+ intents and healing decisions. It integrates:
6
+
7
+ - Idempotent quota consumption (usage tracker)
8
+ - Tenant isolation (tenant_id from request.state, with fallback to X-Tenant-ID header)
9
+ - Auditable decision logging (DecisionAuditLogDB)
10
+ - Pricing telemetry (optional, to arf‑pricing‑calculator)
11
+ - OpenTelemetry tracing
12
+ - Optional Rust execution ladder for mechanical enforcement
13
+ - **v4.3.1**: Full governance loop produces a Bayesian HealingIntent with skill
14
+ posterior parameters (α, β) for the enterprise SkillGate.
15
+ Includes persistent stability controller and temporal monitor for
16
+ cross‑request state accumulation, and a merging policy evaluator that
17
+ respects both external and internal policy violations.
18
+ Healing endpoint now optionally accepts skill context for Bayesian
19
+ utility‑aware action selection.
20
+ - **v4.3.2**: Passes criticality parameter for dynamic gate tuning (Feature 3).
21
+ Internal API key verification added to secure direct access.
22
+ """
23
+
24
  from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks, Header
25
  from fastapi.encoders import jsonable_encoder
26
  from sqlalchemy.orm import Session
 
 
 
 
 
 
27
  from pydantic import BaseModel
28
  import uuid
29
  import logging
30
  import time
31
+ import datetime
32
+ from typing import Optional, Dict, Any, List
33
 
34
+ from app.models.infrastructure_intents import InfrastructureIntentRequest
35
+ from app.services.intent_adapter import to_oss_intent
36
+ from app.services.risk_service import evaluate_intent_full, evaluate_healing_decision
37
+ from app.services.intent_store import save_evaluated_intent
38
+ from app.services.outcome_service import record_outcome
39
+ from app.api.deps import get_db, get_skill_registry, verify_internal_key # <-- v4.3.2
40
+ from app.database.models_intents import DecisionAuditLogDB, TenantDB
41
  from agentic_reliability_framework.core.models.event import ReliabilityEvent
42
+ from agentic_reliability_framework.core.governance.healing_intent import HealingIntent
43
+ from agentic_reliability_framework.core.governance.policies import (
44
+ PolicyEvaluator,
45
+ allow_all,
46
+ )
47
 
48
+ # ===== USAGE TRACKER =====
49
  import app.core.usage_tracker
50
  from app.core.usage_tracker import UsageRecord
51
 
52
+ # ===== PRICING CALCULATOR =====
53
  try:
54
  from arf_pricing_calculator.storage.buffer import add_event
55
  PRICING_AVAILABLE = True
 
57
  PRICING_AVAILABLE = False
58
  add_event = None
59
 
60
+ # ===== RUST EXECUTION LADDER (optional) =====
61
+ try:
62
+ from arf_enterprise.execution_ladder import ExecutionLadder
63
+ RUST_AVAILABLE = True
64
+ except ImportError:
65
+ RUST_AVAILABLE = False
66
+ ExecutionLadder = None
67
+
68
+ # ===== OPEN TELEMETRY =====
69
  try:
70
  from opentelemetry import trace
71
  from opentelemetry.trace import Status, StatusCode
 
76
  _tracer = None
77
 
78
  logger = logging.getLogger(__name__)
79
+
80
+ # v4.3.2: protect all governance endpoints with internal API key verification
81
+ router = APIRouter(dependencies=[Depends(verify_internal_key)])
82
 
83
 
84
  class OutcomeRequest(BaseModel):
 
86
  success: bool
87
  recorded_by: str
88
  notes: str = ""
89
+ # v4.3.1: optional skill provenance for reliability feedback
90
+ skill_id: Optional[str] = None
91
+ skill_version: Optional[int] = None
92
 
93
 
94
  class HealingDecisionRequest(BaseModel):
95
  event: ReliabilityEvent
96
+ # v4.3.1: optional skill context for Bayesian utility
97
+ skill_id: Optional[str] = None
98
+ skill_version: Optional[int] = None
99
+
100
+
101
+ # --------------------------------------------------------------------------
102
+ # Helper: write audit log (idempotent)
103
+ # --------------------------------------------------------------------------
104
+ async def write_audit_log(
105
+ db: Session,
106
+ tenant_id: str,
107
+ deterministic_id: str,
108
+ healing_intent: Dict[str, Any],
109
+ trace_id: Optional[str] = None,
110
+ idempotency_key: Optional[str] = None,
111
+ ) -> None:
112
+ """
113
+ Store a governance decision in the immutable audit log.
114
+ Idempotent on (tenant_id, deterministic_id) – if already exists, skip.
115
+ """
116
+ # Check if already logged (idempotency)
117
+ existing = db.query(DecisionAuditLogDB).filter(
118
+ DecisionAuditLogDB.tenant_id == tenant_id,
119
+ DecisionAuditLogDB.deterministic_id == deterministic_id
120
+ ).first()
121
+ if existing:
122
+ logger.info(f"Audit log already exists for {deterministic_id}, skipping.")
123
+ return
124
+
125
+ # Extract fields that are actually present in DecisionAuditLogDB
126
+ risk_score = healing_intent.get("risk_score", 0.5)
127
+ action = healing_intent.get("recommended_action", "deny")
128
+ justification = healing_intent.get("justification", "")
129
+ metadata = healing_intent.get("metadata", {})
130
+ memory_success_rate = metadata.get("memory_success_rate")
131
+ memory_weight = metadata.get("memory_weight")
132
+ counterfactual = metadata.get("counterfactual")
133
+
134
+ audit_entry = DecisionAuditLogDB(
135
+ tenant_id=tenant_id,
136
+ deterministic_id=deterministic_id,
137
+ timestamp=datetime.datetime.utcnow(),
138
+ risk_score=risk_score,
139
+ action=action,
140
+ justification=justification,
141
+ memory_success_rate=memory_success_rate,
142
+ memory_weight=memory_weight,
143
+ counterfactual=counterfactual,
144
+ trace_id=trace_id,
145
+ )
146
+ db.add(audit_entry)
147
+ db.commit()
148
+ logger.info(f"Audit log written for {deterministic_id}")
149
 
150
 
151
+ # --------------------------------------------------------------------------
152
+ # Policy evaluator that merges external violations with internal checks
153
+ # --------------------------------------------------------------------------
154
+ class MergingPolicyEvaluator(PolicyEvaluator):
155
+ """
156
+ A policy evaluator that combines a base evaluator (the governance loop's
157
+ own policy tree) with a set of pre‑computed violations (e.g., from an
158
+ external Rust enforcer or the request body). The effective violation list
159
+ is the union of both sources, preserving order and removing duplicates.
160
+ """
161
+ def __init__(self, base_evaluator: PolicyEvaluator, pre_violations: List[str]):
162
+ # We must call the PolicyEvaluator constructor with a root policy,
163
+ # but the base evaluator will be used for actual evaluation.
164
+ super().__init__(base_evaluator.get_root_policy())
165
+ self._base = base_evaluator
166
+ self._pre = list(pre_violations)
167
+
168
+ def evaluate(self, intent, context=None):
169
+ base_violations = self._base.evaluate(intent, context)
170
+ # Merge with pre‑computed violations, preserving order and removing duplicates
171
+ merged = []
172
+ seen = set()
173
+ for v in self._pre:
174
+ if v not in seen:
175
+ merged.append(v)
176
+ seen.add(v)
177
+ for v in base_violations:
178
+ if v not in seen:
179
+ merged.append(v)
180
+ seen.add(v)
181
+ return merged
182
+
183
+ def get_root_policy(self):
184
+ return self._base.get_root_policy()
185
+
186
+
187
+ # --------------------------------------------------------------------------
188
+ # Endpoint: evaluate infrastructure intent
189
+ # --------------------------------------------------------------------------
190
  @router.post("/intents/evaluate")
191
  async def evaluate_intent_endpoint(
192
  request: Request,
 
194
  background_tasks: BackgroundTasks,
195
  db: Session = Depends(get_db),
196
  idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
197
+ skill_registry = Depends(get_skill_registry), # v4.3.1
198
  ):
199
  """
200
+ Evaluate an infrastructure intent with idempotency, tenant isolation,
201
+ full governance loop analysis, Bayesian skill posterior injection,
202
+ and optional criticality parameter for dynamic gate tuning (v4.3.2).
203
  """
 
204
  span = None
205
  if OTEL_AVAILABLE and _tracer:
206
  span = _tracer.start_span("governance.evaluate_intent")
 
212
  if not api_key:
213
  api_key = request.query_params.get("api_key", "unknown")
214
 
215
+ # Get tenant_id from request.state or fallback to X-Tenant-ID header
216
+ tenant_id = getattr(request.state, "tenant_id", None)
217
+ if not tenant_id:
218
+ tenant_id = request.headers.get("X-Tenant-ID")
219
+ if not tenant_id:
220
+ if span:
221
+ span.set_status(Status(StatusCode.ERROR, "Missing tenant_id"))
222
+ span.end()
223
+ raise HTTPException(status_code=403, detail="Tenant not identified")
224
+
225
  current_tracker = app.core.usage_tracker.tracker
226
  if current_tracker is None:
227
  if span:
228
  span.set_status(Status(StatusCode.ERROR, "tracker unavailable"))
229
  span.end()
230
+ raise HTTPException(status_code=503, detail="Usage tracking service unavailable")
 
231
 
232
  record = UsageRecord(
233
  api_key=api_key,
 
248
  if existing_response:
249
  return existing_response
250
  else:
251
+ raise HTTPException(status_code=429, detail="Monthly evaluation quota exceeded")
 
252
 
253
  try:
254
  oss_intent = to_oss_intent(intent_req)
255
  risk_engine = request.app.state.risk_engine
256
+
257
+ # Build the base policy evaluator from the app's policy engine (if available)
258
+ policy_engine = getattr(request.app.state, "policy_engine", None)
259
+ if policy_engine is not None and hasattr(policy_engine, 'root_policy'):
260
+ base_evaluator = PolicyEvaluator(policy_engine.root_policy)
261
+ else:
262
+ base_evaluator = PolicyEvaluator(allow_all())
263
+
264
+ # Wrap it to also include the pre‑computed violations from the request
265
+ policy_evaluator = MergingPolicyEvaluator(
266
+ base_evaluator,
267
+ intent_req.policy_violations
268
+ )
269
+
270
+ # Optional components from app state
271
+ memory = getattr(request.app.state, "rag_graph", None)
272
+ hallucination_probe = getattr(request.app.state, "epistemic_probe", None)
273
+ predictive_engine = getattr(request.app.state, "predictive_engine", None)
274
+ business_calculator = getattr(request.app.state, "business_calculator", None)
275
+
276
+ # Stateful monitors (v4.3.1)
277
+ stability_controller = getattr(request.app.state, "stability_controller", None)
278
+ temporal_monitor = getattr(request.app.state, "temporal_monitor", None)
279
+
280
+ # Run the full governance loop, injecting skill context and criticality if present
281
+ result = evaluate_intent_full(
282
  intent=oss_intent,
283
+ risk_engine=risk_engine,
284
+ policy_evaluator=policy_evaluator,
285
+ memory=memory,
286
+ hallucination_probe=hallucination_probe,
287
+ predictive_engine=predictive_engine,
288
+ business_calculator=business_calculator,
289
+ stability_controller=stability_controller,
290
+ temporal_monitor=temporal_monitor,
291
+ skill_id=intent_req.skill_id,
292
+ skill_registry=skill_registry,
293
+ tenant_id=tenant_id,
294
+ criticality=intent_req.criticality, # v4.3.2
295
  )
296
 
297
  if span:
298
  span.set_attribute("risk_score", result["risk_score"])
 
299
 
300
+ deterministic_id = result.get("deterministic_id", str(uuid.uuid4()))
301
  api_payload = jsonable_encoder(intent_req.model_dump())
302
  oss_payload = jsonable_encoder(oss_intent.model_dump())
303
 
304
  save_evaluated_intent(
305
  db=db,
306
  deterministic_id=deterministic_id,
307
+ tenant_id=tenant_id,
308
  intent_type=intent_req.intent_type,
309
  api_payload=api_payload,
310
  oss_payload=oss_payload,
311
  environment=str(intent_req.environment),
312
+ risk_score=result["risk_score"],
313
  )
314
 
315
  result["intent_id"] = deterministic_id
316
  response_data = result
317
 
318
+ # ---- Write audit log (asynchronously) ----
319
+ healing_intent_dict = result.get("healing_intent", result)
320
+ background_tasks.add_task(
321
+ write_audit_log,
322
+ db=db,
323
+ tenant_id=tenant_id,
324
+ deterministic_id=deterministic_id,
325
+ healing_intent=healing_intent_dict,
326
+ trace_id=span.get_span_context().trace_id if span else None,
327
+ idempotency_key=idempotency_key,
328
+ )
329
+
330
  if current_tracker:
331
  background_tasks.add_task(
332
  current_tracker._insert_audit_log,
 
363
  raise HTTPException(status_code=500, detail=error_msg)
364
 
365
 
366
+ # --------------------------------------------------------------------------
367
+ # Endpoint: record outcome (unchanged)
368
+ # --------------------------------------------------------------------------
369
  @router.post("/intents/outcome")
370
  async def record_outcome_endpoint(
371
  request: Request,
372
  outcome: OutcomeRequest,
373
  db: Session = Depends(get_db),
374
  idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
375
+ skill_registry = Depends(get_skill_registry),
376
  ):
377
+ """Record an outcome for a previously evaluated intent."""
 
 
 
 
378
  try:
379
  risk_engine = request.app.state.risk_engine
380
  outcome_record = record_outcome(
 
385
  notes=outcome.notes,
386
  risk_engine=risk_engine,
387
  idempotency_key=idempotency_key,
388
+ skill_id=outcome.skill_id,
389
+ skill_version=outcome.skill_version,
390
+ skill_registry=skill_registry,
391
  )
392
 
393
  if PRICING_AVAILABLE and add_event is not None:
 
399
  "source": "arf_api_outcome"
400
  }
401
  add_event(event)
402
+ logger.info(f"Added outcome to pricing buffer for intent {outcome.deterministic_id}")
 
 
403
  except Exception as e:
404
+ logger.warning(f"Failed to update pricing buffer for intent {outcome.deterministic_id}: {e}")
 
 
405
 
406
  return {"message": "Outcome recorded", "outcome_id": outcome_record.id}
407
  except Exception as e:
408
  raise HTTPException(status_code=500, detail=str(e))
409
 
410
 
411
+ # --------------------------------------------------------------------------
412
+ # Endpoint: evaluate healing decision (now with skill context)
413
+ # --------------------------------------------------------------------------
414
  @router.post("/healing/evaluate")
415
  async def evaluate_healing_decision_endpoint(
416
  request: Request,
417
  decision_req: HealingDecisionRequest,
418
  background_tasks: BackgroundTasks,
419
+ db: Session = Depends(get_db),
420
  idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
421
+ skill_registry = Depends(get_skill_registry), # v4.3.1
422
  ):
423
  """
424
+ Evaluate a healing decision, audit it, optionally enforce via Rust ladder,
425
+ and now incorporate Bayesian skill reliability if skill context is provided.
426
  """
 
427
  span = None
428
  if OTEL_AVAILABLE and _tracer:
429
  span = _tracer.start_span("governance.evaluate_healing")
 
434
  if not api_key:
435
  api_key = request.query_params.get("api_key", "unknown")
436
 
437
+ tenant_id = getattr(request.state, "tenant_id", None)
438
+ if not tenant_id:
439
+ tenant_id = request.headers.get("X-Tenant-ID")
440
+ if not tenant_id:
441
+ if span:
442
+ span.set_status(Status(StatusCode.ERROR, "Missing tenant_id"))
443
+ span.end()
444
+ raise HTTPException(status_code=403, detail="Tenant not identified")
445
+
446
  current_tracker = app.core.usage_tracker.tracker
447
  if current_tracker is None:
448
  if span:
449
  span.set_status(Status(StatusCode.ERROR, "tracker unavailable"))
450
  span.end()
451
+ raise HTTPException(status_code=503, detail="Usage tracking service unavailable")
 
452
 
453
  record = UsageRecord(
454
  api_key=api_key,
 
469
  if existing_response:
470
  return existing_response
471
  else:
472
+ raise HTTPException(status_code=429, detail="Monthly evaluation quota exceeded")
 
473
 
474
  try:
475
  policy_engine = request.app.state.policy_engine
 
484
  rag_graph=rag_graph,
485
  model=model,
486
  tokenizer=tokenizer,
487
+ # v4.3.1: pass skill context if provided
488
+ skill_id=decision_req.skill_id,
489
+ skill_version=decision_req.skill_version,
490
+ skill_registry=skill_registry,
491
+ )
492
+
493
+ # ---- Optional Rust enforcement ----
494
+ if RUST_AVAILABLE and response_data.get("recommended_action") == "approve":
495
+ try:
496
+ intent_dict = response_data.get("healing_intent", response_data)
497
+ ladder = ExecutionLadder()
498
+ rust_result = ladder.evaluate(intent_dict)
499
+ if not rust_result.get("allowed", False):
500
+ response_data["recommended_action"] = "escalate"
501
+ response_data["justification"] = (
502
+ f"Rust enforcement blocked: {rust_result.get('reason', 'gate failure')}"
503
+ )
504
+ response_data["rust_result"] = rust_result
505
+ logger.warning(f"Rust enforcement overrode approval: {rust_result}")
506
+ except Exception as e:
507
+ logger.warning(f"Rust enforcement failed: {e}")
508
+
509
+ # ---- Write audit log (asynchronously) ----
510
+ deterministic_id = response_data.get("intent_id", str(uuid.uuid4()))
511
+ healing_intent_dict = response_data.get("healing_intent", response_data)
512
+ background_tasks.add_task(
513
+ write_audit_log,
514
+ db=db,
515
+ tenant_id=tenant_id,
516
+ deterministic_id=deterministic_id,
517
+ healing_intent=healing_intent_dict,
518
+ trace_id=span.get_span_context().trace_id if span else None,
519
+ idempotency_key=idempotency_key,
520
  )
521
 
522
  if span:
app/api/routes_incidents.py CHANGED
@@ -198,7 +198,7 @@ async def evaluate_incident(
198
  ),
199
  "confidence": 1.0 - result.get("uncertainty", 0.0),
200
  "risk_score": result["risk_score"],
201
- "status": "oss_advisory_only",
202
  }
203
 
204
  response_data = {
 
198
  ),
199
  "confidence": 1.0 - result.get("uncertainty", 0.0),
200
  "risk_score": result["risk_score"],
201
+ "status": "success",
202
  }
203
 
204
  response_data = {
app/api/routes_users.py CHANGED
@@ -1,12 +1,17 @@
1
  """
2
- User endpoints – registration and quota information.
3
  """
4
 
5
  import uuid
6
- from fastapi import APIRouter, Depends, HTTPException, Request
 
 
7
  from slowapi import Limiter
8
  from slowapi.util import get_remote_address
 
9
  from app.core.usage_tracker import tracker, enforce_quota, Tier
 
 
10
 
11
  router = APIRouter(prefix="/users", tags=["users"])
12
 
@@ -16,43 +21,93 @@ limiter = Limiter(key_func=get_remote_address, default_limits=["5/hour"])
16
 
17
  @router.post("/register")
18
  @limiter.limit("5/hour")
19
- async def register_user(request: Request):
 
 
 
 
20
  """
21
- Public endpoint to create a new free‑tier API key.
22
  Rate‑limited to 5 requests per hour per IP address.
23
  """
24
  if tracker is None:
25
- raise HTTPException(
26
- status_code=503,
27
- detail="Usage tracking not available")
28
 
29
- # Generate a new API key
30
- new_key = f"sk_free_{uuid.uuid4().hex[:24]}"
 
 
 
 
 
 
 
 
 
 
31
 
32
- # Store it as FREE tier
33
- success = tracker.get_or_create_api_key(new_key, Tier.FREE)
 
34
  if not success:
 
 
 
35
  raise HTTPException(status_code=500, detail="Failed to create API key")
36
 
37
  return {
38
  "api_key": new_key,
 
39
  "tier": "free",
40
- "message": "API key created. Store it securely – you won't see it again."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
 
43
  @router.get("/quota")
44
  async def get_user_quota(
45
- request: Request,
46
- quota: dict = Depends(enforce_quota)):
 
47
  """
48
- Return the current user's tier and remaining evaluation quota.
49
  Requires API key in Authorization header.
50
  """
51
  tier = quota["tier"]
52
  remaining = quota["remaining"]
53
  limit = tier.monthly_evaluation_limit if tier else None
 
54
 
55
  return {
 
56
  "tier": tier.value,
57
  "remaining": remaining,
58
  "limit": limit,
 
1
  """
2
+ User endpoints – registration, tenant creation, quota information.
3
  """
4
 
5
  import uuid
6
+ from datetime import datetime
7
+ from fastapi import APIRouter, Depends, HTTPException, Request, Query
8
+ from sqlalchemy.orm import Session
9
  from slowapi import Limiter
10
  from slowapi.util import get_remote_address
11
+
12
  from app.core.usage_tracker import tracker, enforce_quota, Tier
13
+ from app.api.deps import get_db
14
+ from app.database.models_intents import TenantDB # <-- NEW
15
 
16
  router = APIRouter(prefix="/users", tags=["users"])
17
 
 
21
 
22
  @router.post("/register")
23
  @limiter.limit("5/hour")
24
+ async def register_user(
25
+ request: Request,
26
+ db: Session = Depends(get_db),
27
+ org_name: str = Query(None, description="Optional organisation name for the new tenant"),
28
+ ):
29
  """
30
+ Public endpoint to create a new free‑tier API key and a new tenant.
31
  Rate‑limited to 5 requests per hour per IP address.
32
  """
33
  if tracker is None:
34
+ raise HTTPException(status_code=503, detail="Usage tracking service not initialised")
 
 
35
 
36
+ # 1. Create a new tenant in the main database
37
+ tenant_id = str(uuid.uuid4())
38
+ name = org_name or "Default Organization"
39
+ new_tenant = TenantDB(
40
+ id=tenant_id,
41
+ name=name,
42
+ created_at=datetime.utcnow(),
43
+ created_by="self_service"
44
+ )
45
+ db.add(new_tenant)
46
+ db.commit()
47
+ db.refresh(new_tenant)
48
 
49
+ # 2. Generate a new API key for this tenant
50
+ new_key = f"sk_free_{uuid.uuid4().hex[:24]}"
51
+ success = tracker.get_or_create_api_key(api_key=new_key, tenant_id=tenant_id, tier=Tier.FREE)
52
  if not success:
53
+ # Rollback tenant creation if key creation fails
54
+ db.delete(new_tenant)
55
+ db.commit()
56
  raise HTTPException(status_code=500, detail="Failed to create API key")
57
 
58
  return {
59
  "api_key": new_key,
60
+ "tenant_id": tenant_id,
61
  "tier": "free",
62
+ "organization": name,
63
+ "message": "API key and tenant created. Store the key securely – you won't see it again."
64
+ }
65
+
66
+
67
+ @router.get("/me")
68
+ async def get_current_user_info(
69
+ request: Request,
70
+ quota: dict = Depends(enforce_quota),
71
+ db: Session = Depends(get_db),
72
+ ):
73
+ """
74
+ Return information about the current user's tenant and quota.
75
+ Requires API key in Authorization header.
76
+ """
77
+ tenant_id = quota.get("tenant_id")
78
+ if not tenant_id:
79
+ raise HTTPException(status_code=403, detail="No tenant associated with this API key")
80
+
81
+ tenant = db.query(TenantDB).filter(TenantDB.id == tenant_id).first()
82
+ if not tenant:
83
+ raise HTTPException(status_code=404, detail="Tenant not found")
84
+
85
+ return {
86
+ "tenant_id": tenant_id,
87
+ "organization": tenant.name,
88
+ "created_at": tenant.created_at.isoformat() if tenant.created_at else None,
89
+ "tier": quota["tier"].value,
90
+ "remaining": quota["remaining"],
91
+ "limit": quota["limit"],
92
+ }
93
 
94
 
95
  @router.get("/quota")
96
  async def get_user_quota(
97
+ request: Request,
98
+ quota: dict = Depends(enforce_quota),
99
+ ):
100
  """
101
+ Return the current user's tier, remaining quota, and tenant ID.
102
  Requires API key in Authorization header.
103
  """
104
  tier = quota["tier"]
105
  remaining = quota["remaining"]
106
  limit = tier.monthly_evaluation_limit if tier else None
107
+ tenant_id = quota.get("tenant_id")
108
 
109
  return {
110
+ "tenant_id": tenant_id,
111
  "tier": tier.value,
112
  "remaining": remaining,
113
  "limit": limit,
app/core/usage_tracker.py CHANGED
@@ -1,8 +1,10 @@
1
  """
2
  Usage Tracker for ARF API – quotas, tiers, and audit logging.
3
  Thread‑safe, atomic quota consumption, idempotent, fail‑closed.
4
- """
5
 
 
 
 
6
  import json
7
  import sqlite3
8
  import threading
@@ -10,7 +12,7 @@ import time
10
  from contextlib import contextmanager
11
  from datetime import datetime, timedelta
12
  from dataclasses import dataclass
13
- from typing import Dict, Any, Optional, List, Tuple
14
  from enum import Enum
15
  from fastapi import BackgroundTasks, HTTPException, Request
16
 
@@ -24,6 +26,7 @@ except ImportError:
24
 
25
 
26
  class Tier(str, Enum):
 
27
  FREE = "free"
28
  PRO = "pro"
29
  PREMIUM = "premium"
@@ -31,16 +34,18 @@ class Tier(str, Enum):
31
 
32
  @property
33
  def monthly_evaluation_limit(self) -> Optional[int]:
 
34
  limits = {
35
  Tier.FREE: 1000,
36
  Tier.PRO: 10_000,
37
  Tier.PREMIUM: 50_000,
38
- Tier.ENTERPRISE: None, # unlimited
39
  }
40
  return limits[self]
41
 
42
  @property
43
  def audit_log_retention_days(self) -> int:
 
44
  retention = {
45
  Tier.FREE: 7,
46
  Tier.PRO: 30,
@@ -52,7 +57,7 @@ class Tier(str, Enum):
52
 
53
  @dataclass
54
  class UsageRecord:
55
- """Single evaluation usage record."""
56
  api_key: str
57
  tier: Tier
58
  timestamp: float
@@ -66,6 +71,7 @@ class UsageRecord:
66
  class UsageTracker:
67
  """
68
  Thread‑safe usage tracker with atomic quota consumption and idempotency.
 
69
  """
70
 
71
  def __init__(self, db_path: str = "arf_usage.db",
@@ -78,12 +84,11 @@ class UsageTracker:
78
  if redis_url and REDIS_AVAILABLE:
79
  self._redis_client = redis.from_url(redis_url)
80
  elif redis_url:
81
- raise ImportError(
82
- "Redis client not installed. Run: pip install redis")
83
 
84
  @contextmanager
85
  def _get_conn(self):
86
- """Get a thread‑local SQLite connection with write‑ahead logging and immediate transactions."""
87
  if not hasattr(self._local, "conn"):
88
  self._local.conn = sqlite3.connect(
89
  self.db_path, check_same_thread=False, isolation_level=None)
@@ -92,10 +97,13 @@ class UsageTracker:
92
  yield self._local.conn
93
 
94
  def _init_db(self):
 
95
  with self._get_conn() as conn:
 
96
  conn.execute("""
97
  CREATE TABLE IF NOT EXISTS api_keys (
98
  key TEXT PRIMARY KEY,
 
99
  tier TEXT NOT NULL,
100
  created_at REAL NOT NULL,
101
  last_used_at REAL,
@@ -139,16 +147,31 @@ class UsageTracker:
139
  def _get_month_key(self) -> str:
140
  return datetime.now().strftime("%Y-%m")
141
 
142
- def get_or_create_api_key(self, key: str, tier: Tier = Tier.FREE) -> bool:
143
- """Register a new API key. Returns True if key exists or was created."""
 
 
 
 
 
 
 
 
 
 
144
  with self._get_conn() as conn:
145
  row = conn.execute(
146
  "SELECT key FROM api_keys WHERE key = ?", (key,)).fetchone()
147
  if row:
 
 
 
 
 
148
  return True
149
  conn.execute(
150
- "INSERT INTO api_keys (key, tier, created_at, is_active) VALUES (?, ?, ?, ?)",
151
- (key, tier.value, time.time(), 1)
152
  )
153
  conn.commit()
154
  return True
@@ -164,6 +187,17 @@ class UsageTracker:
164
  return None
165
  return Tier(row["tier"])
166
 
 
 
 
 
 
 
 
 
 
 
 
167
  def update_api_key_tier(self, api_key: str, new_tier: Tier) -> bool:
168
  """Update the tier of an existing API key. Returns True if successful."""
169
  with self._get_conn() as conn:
@@ -173,41 +207,28 @@ class UsageTracker:
173
  return False
174
  conn.execute(
175
  "UPDATE api_keys SET tier = ? WHERE key = ?",
176
- (new_tier.value,
177
- api_key))
178
  conn.commit()
179
  return True
180
 
181
  # --------------------------------------------------------------------------
182
- # Atomic quota consumption
183
  # --------------------------------------------------------------------------
184
- def _consume_quota_atomic_sqlite(
185
- self,
186
- api_key: str,
187
- tier: Tier,
188
- month: str) -> bool: # noqa: E501
189
- """
190
- Atomically increment counter only if under limit.
191
- Returns True if quota was consumed, False if limit reached.
192
- """
193
  limit = tier.monthly_evaluation_limit
194
  if limit is None:
195
- # Unlimited – still increment for tracking but always succeed
196
  with self._get_conn() as conn:
197
  conn.execute(
198
- """INSERT INTO monthly_counts (api_key, year_month, count)
199
- VALUES (?, ?, 1)
200
- ON CONFLICT(api_key, year_month) DO UPDATE SET count = count + 1""",
201
  (api_key, month)
202
  )
203
  conn.commit()
204
  return True
205
 
206
- # Use BEGIN IMMEDIATE to lock the database for the transaction
207
  with self._get_conn() as conn:
208
  conn.execute("BEGIN IMMEDIATE")
209
  try:
210
- # Get current count (or 0)
211
  row = conn.execute(
212
  "SELECT count FROM monthly_counts WHERE api_key = ? AND year_month = ?",
213
  (api_key, month)
@@ -216,11 +237,9 @@ class UsageTracker:
216
  if current >= limit:
217
  conn.rollback()
218
  return False
219
- # Increment
220
  conn.execute(
221
- """INSERT INTO monthly_counts (api_key, year_month, count)
222
- VALUES (?, ?, 1)
223
- ON CONFLICT(api_key, year_month) DO UPDATE SET count = count + 1""",
224
  (api_key, month)
225
  )
226
  conn.commit()
@@ -229,15 +248,9 @@ class UsageTracker:
229
  conn.rollback()
230
  raise
231
 
232
- def _consume_quota_atomic_redis(
233
- self,
234
- api_key: str,
235
- tier: Tier,
236
- month: str) -> bool:
237
- """Atomic Lua script for Redis: INCR only if below limit."""
238
  limit = tier.monthly_evaluation_limit
239
  if limit is None:
240
- # Unlimited – just increment and return True
241
  redis_key = f"arf:quota:{api_key}:{month}"
242
  self._redis_client.incr(redis_key)
243
  self._redis_client.expire(redis_key, timedelta(days=31))
@@ -251,7 +264,7 @@ class UsageTracker:
251
  return 0
252
  end
253
  local new = redis.call('INCR', key)
254
- redis.call('EXPIRE', key, 2678400) -- 31 days
255
  return 1
256
  """
257
  redis_key = f"arf:quota:{api_key}:{month}"
@@ -259,144 +272,83 @@ class UsageTracker:
259
  return result == 1
260
 
261
  # --------------------------------------------------------------------------
262
- # Idempotency handling
263
  # --------------------------------------------------------------------------
264
  def _is_idempotent_key_used(self, key: str) -> bool:
265
- """Check if idempotency key already processed."""
266
  with self._get_conn() as conn:
267
  row = conn.execute(
268
  "SELECT 1 FROM idempotency_keys WHERE key = ?", (key,)).fetchone()
269
  return row is not None
270
 
271
  def _mark_idempotent_key_used(self, key: str, ttl_seconds: int = 86400):
272
- """Store idempotency key with expiration (cleanup later)."""
273
  with self._get_conn() as conn:
274
  conn.execute(
275
  "INSERT INTO idempotency_keys (key, consumed_at) VALUES (?, ?)",
276
  (key, time.time())
277
  )
278
  conn.commit()
279
- # Optionally schedule cleanup of old keys (can be done in a background
280
- # thread)
281
 
282
  # --------------------------------------------------------------------------
283
- # Core usage recording (atomic + idempotent)
284
  # --------------------------------------------------------------------------
285
- def consume_quota_and_log(
286
- self,
287
- record: UsageRecord,
288
- idempotency_key: Optional[str] = None,
289
- ) -> Tuple[bool, Optional[Dict[str, Any]]]:
290
- """
291
- Atomically consume quota and insert audit log.
292
- Returns (success, existing_response) where existing_response is not None
293
- only when idempotency_key matched a previous successful call.
294
- """
295
- # Idempotency check (if key provided)
296
- if idempotency_key:
297
- if self._is_idempotent_key_used(idempotency_key):
298
- # Retrieve previous response from audit log (simplified – you may cache full response)
299
- # For full idempotency, we would store the response body in idempotency table.
300
- # Here we return a marker that caller should use cached
301
- # response.
302
- return False, {"idempotent": True,
303
- "message": "Already processed"}
304
 
305
  month = self._get_month_key()
306
- # Atomic quota consumption
307
  if self._redis_client:
308
- quota_ok = self._consume_quota_atomic_redis(
309
- record.api_key, record.tier, month)
310
  else:
311
- quota_ok = self._consume_quota_atomic_sqlite(
312
- record.api_key, record.tier, month)
313
 
314
  if not quota_ok:
315
  return False, None
316
 
317
- # Insert audit log (with idempotency key as unique constraint)
318
  try:
319
  with self._get_conn() as conn:
320
  conn.execute(
321
  """INSERT INTO usage_log
322
- (api_key, tier, timestamp, endpoint,
323
- request_body, response, error, processing_ms,
324
- idempotency_key)
325
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
326
- (record.api_key,
327
- record.tier.value,
328
- record.timestamp,
329
- record.endpoint,
330
- json.dumps(
331
- record.request_body) if record.request_body else None,
332
- json.dumps(
333
- record.response) if record.response else None,
334
- record.error,
335
- record.processing_ms,
336
- idempotency_key,
337
- ))
338
  conn.commit()
339
  except sqlite3.IntegrityError as e:
340
- # Duplicate idempotency_key – already inserted by another
341
- # concurrent request
342
  if "UNIQUE constraint failed: usage_log.idempotency_key" in str(e):
343
- return False, {"idempotent": True,
344
- "message": "Already processed"}
345
  raise
346
 
347
  if idempotency_key:
348
  self._mark_idempotent_key_used(idempotency_key)
349
- # Removed stray # noqa: E501 comment that was wrongly indented here
350
  return True, None
351
 
352
  # --------------------------------------------------------------------------
353
- # Legacy interface (kept for compatibility but deprecated)
354
  # --------------------------------------------------------------------------
355
- def increment_usage_sync(
356
- self,
357
- record: UsageRecord,
358
- idempotency_key: Optional[str] = None) -> bool:
359
- """
360
- Synchronously record usage and increment counter.
361
- Returns True if within quota and recorded, False otherwise.
362
- This method now uses the atomic implementation.
363
- """
364
  success, _ = self.consume_quota_and_log(record, idempotency_key)
365
  return success
366
 
367
- async def increment_usage_async(
368
- self,
369
- record: UsageRecord,
370
- background_tasks: BackgroundTasks,
371
- idempotency_key: Optional[str] = None
372
- ) -> bool:
373
- """
374
- Asynchronously record usage using FastAPI BackgroundTasks.
375
- Still does the atomic check synchronously, then schedules the insert.
376
- """
377
- # First, do atomic quota check (synchronous) – we must ensure we don't double-consume.
378
- # Because background tasks may run later, we still need to reserve quota now.
379
- # Simplified: we call consume_quota_and_log synchronously – that defeats async benefit.
380
- # Better to use a queue or Redis with background processing.
381
- # For this fix, we'll use the sync method (blocking) but still support
382
- # idempotency.
383
  return self.increment_usage_sync(record, idempotency_key)
384
 
385
  # --------------------------------------------------------------------------
386
- # Quota inspection (non‑atomic, for display only)
387
  # --------------------------------------------------------------------------
388
  def get_remaining_quota(self, api_key: str, tier: Tier) -> Optional[int]:
389
- """Return remaining evaluations for the month (non‑atomic, for info only)."""
390
  limit = tier.monthly_evaluation_limit
391
  if limit is None:
392
  return None
393
-
394
  month = self._get_month_key()
395
  if self._redis_client:
396
  redis_key = f"arf:quota:{api_key}:{month}"
397
  count = int(self._redis_client.get(redis_key) or 0)
398
  return max(0, limit - count)
399
-
400
  with self._get_conn() as conn:
401
  row = conn.execute(
402
  "SELECT count FROM monthly_counts WHERE api_key = ? AND year_month = ?",
@@ -406,16 +358,10 @@ class UsageTracker:
406
  return max(0, limit - count)
407
 
408
  # --------------------------------------------------------------------------
409
- # Audit and maintenance
410
  # --------------------------------------------------------------------------
411
- def get_audit_logs(
412
- self,
413
- api_key: str,
414
- start_date: Optional[datetime] = None,
415
- end_date: Optional[datetime] = None,
416
- limit: int = 100,
417
- ) -> List[Dict[str, Any]]:
418
- """Retrieve audit logs for a given API key."""
419
  query = "SELECT * FROM usage_log WHERE api_key = ?"
420
  params = [api_key]
421
  if start_date:
@@ -426,47 +372,36 @@ class UsageTracker:
426
  params.append(end_date.timestamp())
427
  query += " ORDER BY timestamp DESC LIMIT ?"
428
  params.append(limit)
429
-
430
  with self._get_conn() as conn:
431
  rows = conn.execute(query, params).fetchall()
432
  return [dict(row) for row in rows]
433
 
434
  def clean_old_logs(self):
435
- """Delete logs older than retention period for each tier, and old idempotency keys."""
436
  with self._get_conn() as conn:
437
- # Delete old usage logs
438
  for tier in Tier:
439
  retention_days = tier.audit_log_retention_days
440
- if retention_days is None:
441
- continue
442
  cutoff = time.time() - retention_days * 86400
443
  conn.execute(
444
  "DELETE FROM usage_log WHERE tier = ? AND timestamp < ?",
445
  (tier.value, cutoff)
446
  )
447
- # Delete idempotency keys older than 7 days
448
  cutoff = time.time() - 7 * 86400
449
- conn.execute(
450
- "DELETE FROM idempotency_keys WHERE consumed_at < ?", (cutoff,))
451
  conn.commit()
452
 
453
 
454
  # --------------------------------------------------------------------------
455
- # Global instance and FastAPI dependency (fail‑closed)
456
  # --------------------------------------------------------------------------
457
  tracker: Optional[UsageTracker] = None
458
 
459
 
460
- def init_tracker(
461
- db_path: str = "arf_usage.db",
462
- redis_url: Optional[str] = None):
463
- """Initialize the global tracker. Must be called before enforce_quota."""
464
  global tracker
465
  tracker = UsageTracker(db_path, redis_url)
466
 
467
 
468
  def update_key_tier(api_key: str, new_tier: Tier) -> bool:
469
- """Globally accessible helper to update API key tier."""
470
  if tracker is None:
471
  return False
472
  return tracker.update_api_key_tier(api_key, new_tier)
@@ -474,16 +409,11 @@ def update_key_tier(api_key: str, new_tier: Tier) -> bool:
474
 
475
  async def enforce_quota(request: Request, api_key: str = None):
476
  """
477
- Dependency that checks API key and remaining quota.
478
- FAILS CLOSED: if tracker not initialised, raises HTTP 503.
479
  """
480
- # P0 fix: No fallback that allows all requests
481
  if tracker is None:
482
- raise HTTPException(
483
- status_code=503,
484
- detail="Usage tracking service not initialised. Please contact administrator.")
485
 
486
- # Extract API key from header or query
487
  if api_key is None:
488
  auth_header = request.headers.get("Authorization")
489
  if auth_header and auth_header.startswith("Bearer "):
@@ -496,16 +426,19 @@ async def enforce_quota(request: Request, api_key: str = None):
496
 
497
  tier = tracker.get_tier(api_key)
498
  if tier is None:
499
- raise HTTPException(
500
- status_code=403,
501
- detail="Invalid or inactive API key")
502
 
503
  remaining = tracker.get_remaining_quota(api_key, tier)
504
  if remaining is not None and remaining <= 0:
505
- raise HTTPException(status_code=429,
506
- detail="Monthly evaluation quota exceeded")
 
 
 
 
507
 
508
- # Store in request state for later logging (optional)
509
  request.state.api_key = api_key
510
  request.state.tier = tier
511
- return {"api_key": api_key, "tier": tier, "remaining": remaining}
 
 
 
1
  """
2
  Usage Tracker for ARF API – quotas, tiers, and audit logging.
3
  Thread‑safe, atomic quota consumption, idempotent, fail‑closed.
 
4
 
5
+ Extended for multi‑tenancy: each API key is linked to a tenant ID.
6
+ Tenant ID is stored in the `api_keys` table and used for resource isolation.
7
+ """
8
  import json
9
  import sqlite3
10
  import threading
 
12
  from contextlib import contextmanager
13
  from datetime import datetime, timedelta
14
  from dataclasses import dataclass
15
+ from typing import Dict, Any, Optional, List, Tuple, Callable
16
  from enum import Enum
17
  from fastapi import BackgroundTasks, HTTPException, Request
18
 
 
26
 
27
 
28
  class Tier(str, Enum):
29
+ """Pricing tiers with associated quota limits and audit retention."""
30
  FREE = "free"
31
  PRO = "pro"
32
  PREMIUM = "premium"
 
34
 
35
  @property
36
  def monthly_evaluation_limit(self) -> Optional[int]:
37
+ """Monthly evaluation quota. None = unlimited."""
38
  limits = {
39
  Tier.FREE: 1000,
40
  Tier.PRO: 10_000,
41
  Tier.PREMIUM: 50_000,
42
+ Tier.ENTERPRISE: None,
43
  }
44
  return limits[self]
45
 
46
  @property
47
  def audit_log_retention_days(self) -> int:
48
+ """How many days to keep usage and decision audit logs."""
49
  retention = {
50
  Tier.FREE: 7,
51
  Tier.PRO: 30,
 
57
 
58
  @dataclass
59
  class UsageRecord:
60
+ """Single API call usage record (for quota and debugging)."""
61
  api_key: str
62
  tier: Tier
63
  timestamp: float
 
71
  class UsageTracker:
72
  """
73
  Thread‑safe usage tracker with atomic quota consumption and idempotency.
74
+ Extended to support tenant isolation: each API key is linked to a tenant.
75
  """
76
 
77
  def __init__(self, db_path: str = "arf_usage.db",
 
84
  if redis_url and REDIS_AVAILABLE:
85
  self._redis_client = redis.from_url(redis_url)
86
  elif redis_url:
87
+ raise ImportError("Redis client not installed. Run: pip install redis")
 
88
 
89
  @contextmanager
90
  def _get_conn(self):
91
+ """Get a thread‑local SQLite connection with WAL and immediate transactions."""
92
  if not hasattr(self._local, "conn"):
93
  self._local.conn = sqlite3.connect(
94
  self.db_path, check_same_thread=False, isolation_level=None)
 
97
  yield self._local.conn
98
 
99
  def _init_db(self):
100
+ """Initialise SQLite tables with tenant_id support."""
101
  with self._get_conn() as conn:
102
+ # Modified: api_keys now has tenant_id column
103
  conn.execute("""
104
  CREATE TABLE IF NOT EXISTS api_keys (
105
  key TEXT PRIMARY KEY,
106
+ tenant_id TEXT NOT NULL,
107
  tier TEXT NOT NULL,
108
  created_at REAL NOT NULL,
109
  last_used_at REAL,
 
147
  def _get_month_key(self) -> str:
148
  return datetime.now().strftime("%Y-%m")
149
 
150
+ def get_or_create_api_key(self, key: str, tenant_id: str, tier: Tier = Tier.FREE) -> bool:
151
+ """
152
+ Register a new API key for a given tenant.
153
+
154
+ Args:
155
+ key: The API key (plain text, will be hashed in production).
156
+ tenant_id: UUID of the tenant (must already exist in main DB).
157
+ tier: Initial tier for the key.
158
+
159
+ Returns:
160
+ True if key was created (or already exists for the same tenant).
161
+ """
162
  with self._get_conn() as conn:
163
  row = conn.execute(
164
  "SELECT key FROM api_keys WHERE key = ?", (key,)).fetchone()
165
  if row:
166
+ # Key already exists – ensure it belongs to the same tenant
167
+ existing_tenant = conn.execute(
168
+ "SELECT tenant_id FROM api_keys WHERE key = ?", (key,)).fetchone()
169
+ if existing_tenant["tenant_id"] != tenant_id:
170
+ raise ValueError(f"Key {key[:8]}... already belongs to a different tenant.")
171
  return True
172
  conn.execute(
173
+ "INSERT INTO api_keys (key, tenant_id, tier, created_at, is_active) VALUES (?, ?, ?, ?, ?)",
174
+ (key, tenant_id, tier.value, time.time(), 1)
175
  )
176
  conn.commit()
177
  return True
 
187
  return None
188
  return Tier(row["tier"])
189
 
190
+ def get_tenant_id(self, api_key: str) -> Optional[str]:
191
+ """Return the tenant ID associated with the API key, or None if key invalid."""
192
+ with self._get_conn() as conn:
193
+ row = conn.execute(
194
+ "SELECT tenant_id FROM api_keys WHERE key = ? AND is_active = 1",
195
+ (api_key,)
196
+ ).fetchone()
197
+ if not row:
198
+ return None
199
+ return row["tenant_id"]
200
+
201
  def update_api_key_tier(self, api_key: str, new_tier: Tier) -> bool:
202
  """Update the tier of an existing API key. Returns True if successful."""
203
  with self._get_conn() as conn:
 
207
  return False
208
  conn.execute(
209
  "UPDATE api_keys SET tier = ? WHERE key = ?",
210
+ (new_tier.value, api_key))
 
211
  conn.commit()
212
  return True
213
 
214
  # --------------------------------------------------------------------------
215
+ # Atomic quota consumption (unchanged, but uses api_key which links to tenant)
216
  # --------------------------------------------------------------------------
217
+ def _consume_quota_atomic_sqlite(self, api_key: str, tier: Tier, month: str) -> bool:
 
 
 
 
 
 
 
 
218
  limit = tier.monthly_evaluation_limit
219
  if limit is None:
 
220
  with self._get_conn() as conn:
221
  conn.execute(
222
+ "INSERT INTO monthly_counts (api_key, year_month, count) VALUES (?, ?, 1) "
223
+ "ON CONFLICT(api_key, year_month) DO UPDATE SET count = count + 1",
 
224
  (api_key, month)
225
  )
226
  conn.commit()
227
  return True
228
 
 
229
  with self._get_conn() as conn:
230
  conn.execute("BEGIN IMMEDIATE")
231
  try:
 
232
  row = conn.execute(
233
  "SELECT count FROM monthly_counts WHERE api_key = ? AND year_month = ?",
234
  (api_key, month)
 
237
  if current >= limit:
238
  conn.rollback()
239
  return False
 
240
  conn.execute(
241
+ "INSERT INTO monthly_counts (api_key, year_month, count) VALUES (?, ?, 1) "
242
+ "ON CONFLICT(api_key, year_month) DO UPDATE SET count = count + 1",
 
243
  (api_key, month)
244
  )
245
  conn.commit()
 
248
  conn.rollback()
249
  raise
250
 
251
+ def _consume_quota_atomic_redis(self, api_key: str, tier: Tier, month: str) -> bool:
 
 
 
 
 
252
  limit = tier.monthly_evaluation_limit
253
  if limit is None:
 
254
  redis_key = f"arf:quota:{api_key}:{month}"
255
  self._redis_client.incr(redis_key)
256
  self._redis_client.expire(redis_key, timedelta(days=31))
 
264
  return 0
265
  end
266
  local new = redis.call('INCR', key)
267
+ redis.call('EXPIRE', key, 2678400)
268
  return 1
269
  """
270
  redis_key = f"arf:quota:{api_key}:{month}"
 
272
  return result == 1
273
 
274
  # --------------------------------------------------------------------------
275
+ # Idempotency handling (unchanged)
276
  # --------------------------------------------------------------------------
277
  def _is_idempotent_key_used(self, key: str) -> bool:
 
278
  with self._get_conn() as conn:
279
  row = conn.execute(
280
  "SELECT 1 FROM idempotency_keys WHERE key = ?", (key,)).fetchone()
281
  return row is not None
282
 
283
  def _mark_idempotent_key_used(self, key: str, ttl_seconds: int = 86400):
 
284
  with self._get_conn() as conn:
285
  conn.execute(
286
  "INSERT INTO idempotency_keys (key, consumed_at) VALUES (?, ?)",
287
  (key, time.time())
288
  )
289
  conn.commit()
 
 
290
 
291
  # --------------------------------------------------------------------------
292
+ # Core usage recording (atomic + idempotent) – unchanged
293
  # --------------------------------------------------------------------------
294
+ def consume_quota_and_log(self, record: UsageRecord, idempotency_key: Optional[str] = None
295
+ ) -> Tuple[bool, Optional[Dict[str, Any]]]:
296
+ if idempotency_key and self._is_idempotent_key_used(idempotency_key):
297
+ return False, {"idempotent": True, "message": "Already processed"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  month = self._get_month_key()
 
300
  if self._redis_client:
301
+ quota_ok = self._consume_quota_atomic_redis(record.api_key, record.tier, month)
 
302
  else:
303
+ quota_ok = self._consume_quota_atomic_sqlite(record.api_key, record.tier, month)
 
304
 
305
  if not quota_ok:
306
  return False, None
307
 
 
308
  try:
309
  with self._get_conn() as conn:
310
  conn.execute(
311
  """INSERT INTO usage_log
312
+ (api_key, tier, timestamp, endpoint, request_body, response, error, processing_ms, idempotency_key)
 
 
313
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
314
+ (record.api_key, record.tier.value, record.timestamp, record.endpoint,
315
+ json.dumps(record.request_body) if record.request_body else None,
316
+ json.dumps(record.response) if record.response else None,
317
+ record.error, record.processing_ms, idempotency_key)
318
+ )
 
 
 
 
 
 
 
319
  conn.commit()
320
  except sqlite3.IntegrityError as e:
 
 
321
  if "UNIQUE constraint failed: usage_log.idempotency_key" in str(e):
322
+ return False, {"idempotent": True, "message": "Already processed"}
 
323
  raise
324
 
325
  if idempotency_key:
326
  self._mark_idempotent_key_used(idempotency_key)
 
327
  return True, None
328
 
329
  # --------------------------------------------------------------------------
330
+ # Legacy interface (kept for compatibility)
331
  # --------------------------------------------------------------------------
332
+ def increment_usage_sync(self, record: UsageRecord, idempotency_key: Optional[str] = None) -> bool:
 
 
 
 
 
 
 
 
333
  success, _ = self.consume_quota_and_log(record, idempotency_key)
334
  return success
335
 
336
+ async def increment_usage_async(self, record: UsageRecord, background_tasks: BackgroundTasks,
337
+ idempotency_key: Optional[str] = None) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  return self.increment_usage_sync(record, idempotency_key)
339
 
340
  # --------------------------------------------------------------------------
341
+ # Quota inspection
342
  # --------------------------------------------------------------------------
343
  def get_remaining_quota(self, api_key: str, tier: Tier) -> Optional[int]:
 
344
  limit = tier.monthly_evaluation_limit
345
  if limit is None:
346
  return None
 
347
  month = self._get_month_key()
348
  if self._redis_client:
349
  redis_key = f"arf:quota:{api_key}:{month}"
350
  count = int(self._redis_client.get(redis_key) or 0)
351
  return max(0, limit - count)
 
352
  with self._get_conn() as conn:
353
  row = conn.execute(
354
  "SELECT count FROM monthly_counts WHERE api_key = ? AND year_month = ?",
 
358
  return max(0, limit - count)
359
 
360
  # --------------------------------------------------------------------------
361
+ # Audit and maintenance (kept for usage_log)
362
  # --------------------------------------------------------------------------
363
+ def get_audit_logs(self, api_key: str, start_date: Optional[datetime] = None,
364
+ end_date: Optional[datetime] = None, limit: int = 100) -> List[Dict[str, Any]]:
 
 
 
 
 
 
365
  query = "SELECT * FROM usage_log WHERE api_key = ?"
366
  params = [api_key]
367
  if start_date:
 
372
  params.append(end_date.timestamp())
373
  query += " ORDER BY timestamp DESC LIMIT ?"
374
  params.append(limit)
 
375
  with self._get_conn() as conn:
376
  rows = conn.execute(query, params).fetchall()
377
  return [dict(row) for row in rows]
378
 
379
  def clean_old_logs(self):
 
380
  with self._get_conn() as conn:
 
381
  for tier in Tier:
382
  retention_days = tier.audit_log_retention_days
 
 
383
  cutoff = time.time() - retention_days * 86400
384
  conn.execute(
385
  "DELETE FROM usage_log WHERE tier = ? AND timestamp < ?",
386
  (tier.value, cutoff)
387
  )
 
388
  cutoff = time.time() - 7 * 86400
389
+ conn.execute("DELETE FROM idempotency_keys WHERE consumed_at < ?", (cutoff,))
 
390
  conn.commit()
391
 
392
 
393
  # --------------------------------------------------------------------------
394
+ # Global instance and FastAPI dependency
395
  # --------------------------------------------------------------------------
396
  tracker: Optional[UsageTracker] = None
397
 
398
 
399
+ def init_tracker(db_path: str = "arf_usage.db", redis_url: Optional[str] = None):
 
 
 
400
  global tracker
401
  tracker = UsageTracker(db_path, redis_url)
402
 
403
 
404
  def update_key_tier(api_key: str, new_tier: Tier) -> bool:
 
405
  if tracker is None:
406
  return False
407
  return tracker.update_api_key_tier(api_key, new_tier)
 
409
 
410
  async def enforce_quota(request: Request, api_key: str = None):
411
  """
412
+ FastAPI dependency that enforces quota and attaches tenant_id to request state.
 
413
  """
 
414
  if tracker is None:
415
+ raise HTTPException(status_code=503, detail="Usage tracking service not initialised.")
 
 
416
 
 
417
  if api_key is None:
418
  auth_header = request.headers.get("Authorization")
419
  if auth_header and auth_header.startswith("Bearer "):
 
426
 
427
  tier = tracker.get_tier(api_key)
428
  if tier is None:
429
+ raise HTTPException(status_code=403, detail="Invalid or inactive API key")
 
 
430
 
431
  remaining = tracker.get_remaining_quota(api_key, tier)
432
  if remaining is not None and remaining <= 0:
433
+ raise HTTPException(status_code=429, detail="Monthly evaluation quota exceeded")
434
+
435
+ # Retrieve tenant_id
436
+ tenant_id = tracker.get_tenant_id(api_key)
437
+ if not tenant_id:
438
+ raise HTTPException(status_code=403, detail="API key not associated with a tenant")
439
 
 
440
  request.state.api_key = api_key
441
  request.state.tier = tier
442
+ request.state.tenant_id = tenant_id
443
+
444
+ return {"api_key": api_key, "tier": tier, "tenant_id": tenant_id, "remaining": remaining}
app/database/models_intents.py CHANGED
@@ -1,50 +1,182 @@
1
- from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, JSON, Float, ForeignKey, UniqueConstraint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from sqlalchemy.orm import relationship
3
  import datetime
4
  from .base import Base
5
 
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  class IntentDB(Base):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  __tablename__ = "intents"
 
9
  id = Column(Integer, primary_key=True, index=True)
10
- deterministic_id = Column(
11
- String(64),
12
- unique=True,
13
- index=True,
14
- nullable=False)
15
  intent_type = Column(String(64), nullable=False)
16
  payload = Column(JSON, nullable=False)
17
  oss_payload = Column(JSON, nullable=True)
18
  environment = Column(String(32), nullable=True)
19
- created_at = Column(
20
- DateTime,
21
- default=datetime.datetime.utcnow,
22
- nullable=False)
23
  evaluated_at = Column(DateTime, nullable=True)
24
  risk_score = Column(String(32), nullable=True)
25
- outcomes = relationship(
26
- "OutcomeDB",
27
- back_populates="intent",
28
- cascade="all, delete-orphan")
29
 
30
 
31
  class OutcomeDB(Base):
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  __tablename__ = "intent_outcomes"
 
33
  id = Column(Integer, primary_key=True, index=True)
34
- intent_id = Column(
35
- Integer,
36
- ForeignKey(
37
- "intents.id",
38
- ondelete="CASCADE"),
39
- nullable=False)
40
  success = Column(Boolean, nullable=False)
41
  recorded_by = Column(String(128), nullable=True)
42
  notes = Column(Text, nullable=True)
43
- recorded_at = Column(
44
- DateTime,
45
- default=datetime.datetime.utcnow,
46
- nullable=False)
47
  idempotency_key = Column(String(128), unique=True, nullable=True)
 
48
  intent = relationship("IntentDB", back_populates="outcomes")
49
 
50
  __table_args__ = (
@@ -52,24 +184,81 @@ class OutcomeDB(Base):
52
  )
53
 
54
 
55
- # ---------------------------------------------------------------------------
56
- # NEW: Persistence for the conjugate Bayesian state
57
- # ---------------------------------------------------------------------------
 
58
  class BetaStateDB(Base):
59
  """
60
- Stores the per‑category posterior parameters (α, β) of the BetaStore
61
- so that online learning survives API restarts.
 
62
 
63
- Only one row per ActionCategory is expected; the 'category' column is
64
- unique. Updates are performed via merge / upsert.
 
 
 
 
 
65
  """
66
  __tablename__ = "beta_state"
67
 
68
  id = Column(Integer, primary_key=True, index=True)
69
- category = Column(String(32), unique=True, nullable=False, index=True)
 
70
  alpha = Column(Float, nullable=False)
71
  beta = Column(Float, nullable=False)
72
- updated_at = Column(
73
- DateTime,
74
- default=datetime.datetime.utcnow,
75
- onupdate=datetime.datetime.utcnow)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database models for the ARF API Control Plane.
3
+
4
+ This module defines the SQLAlchemy ORM models for:
5
+ - Tenants (multi‑tenant isolation root)
6
+ - API keys (per‑tenant, tier‑based)
7
+ - Usage logs (immutable records of API calls)
8
+ - Intents (InfrastructureIntent evaluations)
9
+ - Outcomes (recorded results of executed intents)
10
+ - Beta state (conjugate Bayesian posteriors per tenant and category)
11
+ - Audit logs (immutable decision records for compliance)
12
+
13
+ All tables include a `tenant_id` column to enforce data partitioning.
14
+ """
15
+
16
+ import uuid
17
+ from sqlalchemy import (
18
+ Column, Integer, String, DateTime, Boolean, Text, JSON,
19
+ Float, ForeignKey, UniqueConstraint, Index
20
+ )
21
  from sqlalchemy.orm import relationship
22
  import datetime
23
  from .base import Base
24
 
25
 
26
+ # ============================================================================
27
+ # Tenant table – root of multi‑tenancy
28
+ # ============================================================================
29
+
30
+ class TenantDB(Base):
31
+ """
32
+ Represents a customer tenant (organisation). All other tables
33
+ reference this table via a foreign key `tenant_id`.
34
+
35
+ Attributes:
36
+ id (str): UUID of the tenant (primary key).
37
+ name (str): Human‑readable organisation name.
38
+ created_at (datetime): UTC timestamp of creation.
39
+ created_by (str, optional): Email or user ID of the creator.
40
+ """
41
+ __tablename__ = "tenants"
42
+
43
+ id = Column(String(64), primary_key=True, index=True)
44
+ name = Column(String(256), nullable=False)
45
+ created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
46
+ created_by = Column(String(128), nullable=True)
47
+
48
+ # Relationships
49
+ api_keys = relationship("APIKeyDB", back_populates="tenant", cascade="all, delete-orphan")
50
+ intents = relationship("IntentDB", back_populates="tenant")
51
+ beta_states = relationship("BetaStateDB", back_populates="tenant")
52
+ audit_logs = relationship("DecisionAuditLogDB", back_populates="tenant")
53
+
54
+
55
+ # ============================================================================
56
+ # API keys (extended with tenant_id)
57
+ # ============================================================================
58
+
59
+ class APIKeyDB(Base):
60
+ """
61
+ Stores API keys for authentication and tiered quota. Each key belongs
62
+ to exactly one tenant. The `tier` determines monthly evaluation limits.
63
+
64
+ Attributes:
65
+ key (str): The hashed API key (primary key).
66
+ tenant_id (str): Foreign key to `tenants.id`.
67
+ tier (str): Tier enumeration value (free, pro, premium, enterprise).
68
+ created_at (datetime): UTC creation time.
69
+ last_used_at (datetime, optional): Timestamp of last successful request.
70
+ is_active (bool): Soft‑delete flag.
71
+ """
72
+ __tablename__ = "api_keys"
73
+
74
+ key = Column(String(256), primary_key=True, index=True)
75
+ tenant_id = Column(String(64), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
76
+ tier = Column(String(32), nullable=False)
77
+ created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
78
+ last_used_at = Column(DateTime, nullable=True)
79
+ is_active = Column(Boolean, default=True, nullable=False)
80
+
81
+ # Relationships
82
+ tenant = relationship("TenantDB", back_populates="api_keys")
83
+ usage_logs = relationship("UsageLogDB", back_populates="api_key_rel", cascade="all, delete-orphan")
84
+
85
+
86
+ # ============================================================================
87
+ # Usage logs – each API call
88
+ # ============================================================================
89
+
90
+ class UsageLogDB(Base):
91
+ """
92
+ Immutable record of each API call for quota tracking and billing.
93
+
94
+ Attributes:
95
+ id (int): Primary key.
96
+ api_key (str): Foreign key to `api_keys.key`.
97
+ tier (str): Tier at the time of the call.
98
+ timestamp (float): Unix timestamp of the request.
99
+ endpoint (str): URL or route of the endpoint hit.
100
+ request_body (JSON, optional): Request payload (sanitised).
101
+ response (JSON, optional): Response metadata (e.g., status code).
102
+ """
103
+ __tablename__ = "usage_logs"
104
+
105
+ id = Column(Integer, primary_key=True, index=True)
106
+ api_key = Column(String(256), ForeignKey("api_keys.key", ondelete="CASCADE"), nullable=False)
107
+ tier = Column(String(32), nullable=False)
108
+ timestamp = Column(Float, nullable=False)
109
+ endpoint = Column(String(512), nullable=True)
110
+ request_body = Column(JSON, nullable=True)
111
+ response = Column(JSON, nullable=True)
112
+
113
+ # Relationship back to API key
114
+ api_key_rel = relationship("APIKeyDB", back_populates="usage_logs")
115
+
116
+
117
+ # ============================================================================
118
+ # Intents (evaluations) – now tenant‑scoped
119
+ # ============================================================================
120
+
121
  class IntentDB(Base):
122
+ """
123
+ Stores each InfrastructureIntent evaluation request and its resulting
124
+ risk score. One‑to‑many with OutcomeDB.
125
+
126
+ Attributes:
127
+ id (int): Auto‑increment primary key.
128
+ deterministic_id (str): Client‑provided idempotency identifier (unique).
129
+ tenant_id (str): Tenant that owns this intent.
130
+ intent_type (str): Type of intent (e.g., "provision_resource").
131
+ payload (JSON): Original API request payload.
132
+ oss_payload (JSON): Canonical OSS intent representation.
133
+ environment (str, optional): Environment label (prod, staging, etc.).
134
+ created_at (datetime): UTC timestamp of evaluation.
135
+ evaluated_at (datetime, optional): When the risk engine processed it.
136
+ risk_score (str, optional): String representation of the risk score.
137
+ """
138
  __tablename__ = "intents"
139
+
140
  id = Column(Integer, primary_key=True, index=True)
141
+ deterministic_id = Column(String(64), unique=True, index=True, nullable=False)
142
+ tenant_id = Column(String(64), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
 
 
 
143
  intent_type = Column(String(64), nullable=False)
144
  payload = Column(JSON, nullable=False)
145
  oss_payload = Column(JSON, nullable=True)
146
  environment = Column(String(32), nullable=True)
147
+ created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
 
 
 
148
  evaluated_at = Column(DateTime, nullable=True)
149
  risk_score = Column(String(32), nullable=True)
150
+
151
+ # Relationships
152
+ tenant = relationship("TenantDB", back_populates="intents")
153
+ outcomes = relationship("OutcomeDB", back_populates="intent", cascade="all, delete-orphan")
154
 
155
 
156
  class OutcomeDB(Base):
157
+ """
158
+ Records the outcome (success/failure) of a previously evaluated intent.
159
+ Only one outcome per intent is allowed (unique constraint on intent_id).
160
+
161
+ Attributes:
162
+ id (int): Primary key.
163
+ intent_id (int): Foreign key to `intents.id`.
164
+ success (bool): Whether the executed action succeeded.
165
+ recorded_by (str, optional): Identity of the caller (e.g., API key owner).
166
+ notes (str, optional): Free‑text notes.
167
+ recorded_at (datetime): UTC timestamp.
168
+ idempotency_key (str, optional): Unique idempotency key for this outcome.
169
+ """
170
  __tablename__ = "intent_outcomes"
171
+
172
  id = Column(Integer, primary_key=True, index=True)
173
+ intent_id = Column(Integer, ForeignKey("intents.id", ondelete="CASCADE"), nullable=False)
 
 
 
 
 
174
  success = Column(Boolean, nullable=False)
175
  recorded_by = Column(String(128), nullable=True)
176
  notes = Column(Text, nullable=True)
177
+ recorded_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
 
 
 
178
  idempotency_key = Column(String(128), unique=True, nullable=True)
179
+
180
  intent = relationship("IntentDB", back_populates="outcomes")
181
 
182
  __table_args__ = (
 
184
  )
185
 
186
 
187
+ # ============================================================================
188
+ # Bayesian conjugate state now per tenant and per category
189
+ # ============================================================================
190
+
191
  class BetaStateDB(Base):
192
  """
193
+ Stores the posterior parameters (α, β) of the conjugate Beta model
194
+ for each (tenant, category) pair. This allows online learning to be
195
+ isolated per customer.
196
 
197
+ Attributes:
198
+ id (int): Primary key.
199
+ tenant_id (str): Tenant that owns this state.
200
+ category (str): ActionCategory value (e.g., "database", "compute").
201
+ alpha (float): α parameter of the Beta distribution.
202
+ beta (float): β parameter of the Beta distribution.
203
+ updated_at (datetime): Last update timestamp (auto‑set).
204
  """
205
  __tablename__ = "beta_state"
206
 
207
  id = Column(Integer, primary_key=True, index=True)
208
+ tenant_id = Column(String(64), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
209
+ category = Column(String(32), nullable=False, index=True)
210
  alpha = Column(Float, nullable=False)
211
  beta = Column(Float, nullable=False)
212
+ updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
213
+
214
+ __table_args__ = (
215
+ UniqueConstraint("tenant_id", "category", name="uq_beta_state_tenant_category"),
216
+ )
217
+
218
+ # Relationships
219
+ tenant = relationship("TenantDB", back_populates="beta_states")
220
+
221
+
222
+ # ============================================================================
223
+ # NEW: Audit log for compliance (immutable decision records)
224
+ # ============================================================================
225
+
226
+ class DecisionAuditLogDB(Base):
227
+ """
228
+ Immutable, tamper‑evident record of every governance decision.
229
+ Designed for compliance (SOC2, ISO) and forensic analysis.
230
+
231
+ Attributes:
232
+ id (str): UUID primary key.
233
+ tenant_id (str): Tenant that owns the decision.
234
+ deterministic_id (str): Intent identifier (idempotency key).
235
+ timestamp (datetime): UTC decision time.
236
+ risk_score (float): Fused Bayesian risk score (0‑1).
237
+ action (str): Selected action (approve, deny, escalate).
238
+ justification (str): Human‑readable explanation.
239
+ memory_success_rate (float, optional): Memory‑based correction value.
240
+ memory_weight (float, optional): Weight assigned to memory.
241
+ counterfactual (JSON, optional): Structured counterfactual explanation.
242
+ trace_id (str, optional): OpenTelemetry trace ID for debugging.
243
+ signature (str, optional): Ed25519 signature for tamper‑proofing.
244
+ """
245
+ __tablename__ = "decision_audit_log"
246
+
247
+ id = Column(String(64), primary_key=True, default=lambda: str(uuid.uuid4()))
248
+ tenant_id = Column(String(64), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
249
+ deterministic_id = Column(String(64), nullable=False, index=True)
250
+ timestamp = Column(DateTime, default=datetime.datetime.utcnow, nullable=False, index=True)
251
+ risk_score = Column(Float, nullable=False)
252
+ action = Column(String(32), nullable=False)
253
+ justification = Column(Text, nullable=False)
254
+ memory_success_rate = Column(Float, nullable=True)
255
+ memory_weight = Column(Float, nullable=True)
256
+ counterfactual = Column(JSON, nullable=True)
257
+ trace_id = Column(String(128), nullable=True)
258
+ signature = Column(String(256), nullable=True)
259
+
260
+ __table_args__ = (
261
+ Index("idx_audit_tenant_time", "tenant_id", "timestamp"),
262
+ )
263
+
264
+ tenant = relationship("TenantDB", back_populates="audit_logs")
app/main.py CHANGED
@@ -9,7 +9,8 @@ enterprise clients, and monitoring infrastructure).
9
  It is responsible for:
10
 
11
  * **Lifetime management** of the Bayesian risk engine, policy engine,
12
- semantic memory (RAG graph), and epistemic models.
 
13
  * **Observability** via optional OpenTelemetry tracing and Prometheus metrics
14
  (the latter exposed automatically by ``prometheus-fastapi-instrumentator``
15
  on ``/metrics``).
@@ -71,6 +72,14 @@ except ImportError:
71
  RAGGraphMemory = None
72
  MemoryConstants = None
73
 
 
 
 
 
 
 
 
 
74
  # ── Usage tracker ────────────────────────────────────────────
75
  from app.core.usage_tracker import init_tracker, tracker, Tier
76
 
@@ -109,11 +118,12 @@ async def lifespan(app: FastAPI):
109
 
110
  Initialisation order:
111
  1. Risk engine (Bayesian scoring + HMC).
112
- 2. Load persisted conjugate posterior state (``beta_state`` table).
113
  3. OpenTelemetry tracing (console exporter by default).
114
  4. Policy engine, RAG memory, and epistemic model.
115
- 5. Usage tracker (SQLite / Redis).
116
- 6. Wilson confidence monitor for Rust enforcer canary promotion.
 
117
  """
118
  logger.info("🚀 Starting ARF API Control Plane")
119
  logger.debug(f"Python path: {sys.path}")
@@ -141,35 +151,31 @@ async def lifespan(app: FastAPI):
141
  logger.exception("💥 Fatal error initializing RiskEngine")
142
  raise RuntimeError("RiskEngine initialization failed") from e
143
 
144
- # ── 2. Persisted Bayesian state ───────────────────────
145
  try:
146
  from app.database.session import SessionLocal
147
- from app.database.models_intents import BetaStateDB
148
  from agentic_reliability_framework.core.governance.risk_engine import ActionCategory
149
 
150
  db = SessionLocal()
151
  try:
152
- rows = db.query(BetaStateDB).all()
153
- if rows:
154
- state = {
155
- ActionCategory(row.category): (row.alpha, row.beta)
156
- for row in rows
157
- }
158
- app.state.risk_engine.beta_store.load_state(state)
159
- logger.info(
160
- "Loaded Bayesian posterior state from database (%d categories).",
161
- len(state),
162
- )
163
- else:
164
- logger.info(
165
- "No persisted Bayesian state found; using default priors."
166
- )
167
  finally:
168
  db.close()
169
  except Exception as e:
170
- logger.warning(
171
- "Could not load Bayesian state from database: %s", e
172
- )
173
 
174
  # ── 3. Tracing (OpenTelemetry) ─────────────────────────
175
  try:
@@ -231,12 +237,27 @@ async def lifespan(app: FastAPI):
231
  )
232
  app.state.epistemic_model = None
233
  app.state.epistemic_tokenizer = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  else:
235
  logger.warning(
236
- "agentic_reliability_framework not installed; risk engine, policy engine, RAG disabled."
237
  )
238
 
239
- # ── 5. Usage tracker ──────────────────────────────────────
240
  usage_tracking_disabled = (
241
  os.getenv("ARF_USAGE_TRACKING", "true").lower() == "false"
242
  )
@@ -273,7 +294,7 @@ async def lifespan(app: FastAPI):
273
  logger.info("Usage tracking disabled by ARF_USAGE_TRACKING=false.")
274
  app.state.usage_tracker = None
275
 
276
- # ── 6. Wilson confidence monitor ──────────────────────────
277
  try:
278
  from app.services.wilson_monitor import update as wilson_update
279
  from prometheus_client import REGISTRY
 
9
  It is responsible for:
10
 
11
  * **Lifetime management** of the Bayesian risk engine, policy engine,
12
+ semantic memory (RAG graph), epistemic models, and (new in v4.3.1)
13
+ the stability controller and temporal reliability monitor.
14
  * **Observability** via optional OpenTelemetry tracing and Prometheus metrics
15
  (the latter exposed automatically by ``prometheus-fastapi-instrumentator``
16
  on ``/metrics``).
 
72
  RAGGraphMemory = None
73
  MemoryConstants = None
74
 
75
+ # ── Stability & temporal monitors ───────────────────────────
76
+ from agentic_reliability_framework.core.governance.stability_controller import (
77
+ LyapunovStabilityController,
78
+ )
79
+ from agentic_reliability_framework.core.temporal_reliability import (
80
+ TemporalReliabilityMonitor,
81
+ )
82
+
83
  # ── Usage tracker ────────────────────────────────────────────
84
  from app.core.usage_tracker import init_tracker, tracker, Tier
85
 
 
118
 
119
  Initialisation order:
120
  1. Risk engine (Bayesian scoring + HMC).
121
+ 2. Load persisted conjugate posterior state per tenant.
122
  3. OpenTelemetry tracing (console exporter by default).
123
  4. Policy engine, RAG memory, and epistemic model.
124
+ 5. Stability controller & temporal monitor (v4.3.1).
125
+ 6. Usage tracker (SQLite / Redis).
126
+ 7. Wilson confidence monitor for Rust enforcer canary promotion.
127
  """
128
  logger.info("🚀 Starting ARF API Control Plane")
129
  logger.debug(f"Python path: {sys.path}")
 
151
  logger.exception("💥 Fatal error initializing RiskEngine")
152
  raise RuntimeError("RiskEngine initialization failed") from e
153
 
154
+ # ── 2. Persisted Bayesian state (PER TENANT) ───────────
155
  try:
156
  from app.database.session import SessionLocal
157
+ from app.database.models_intents import BetaStateDB, TenantDB
158
  from agentic_reliability_framework.core.governance.risk_engine import ActionCategory
159
 
160
  db = SessionLocal()
161
  try:
162
+ # Load all tenants that have beta_state entries (or all tenants)
163
+ tenant_rows = db.query(TenantDB.id).all()
164
+ tenant_ids = [tid for (tid,) in tenant_rows] if tenant_rows else ["__default__"]
165
+
166
+ for tid in tenant_ids:
167
+ rows = db.query(BetaStateDB).filter(BetaStateDB.tenant_id == tid).all()
168
+ if rows:
169
+ state = {ActionCategory(row.category): (row.alpha, row.beta) for row in rows}
170
+ app.state.risk_engine.load_tenant_state(tid, state)
171
+ logger.info(f"Loaded Bayesian state for tenant {tid}: {len(state)} categories.")
172
+ else:
173
+ app.state.risk_engine._ensure_tenant(tid)
174
+ logger.info(f"No persisted state for tenant {tid}; using default priors.")
 
 
175
  finally:
176
  db.close()
177
  except Exception as e:
178
+ logger.warning(f"Could not load tenant Beta states: {e}")
 
 
179
 
180
  # ── 3. Tracing (OpenTelemetry) ─────────────────────────
181
  try:
 
237
  )
238
  app.state.epistemic_model = None
239
  app.state.epistemic_tokenizer = None
240
+
241
+ # ── 5. Stability controller & temporal monitor (v4.3.1) ─
242
+ try:
243
+ app.state.stability_controller = LyapunovStabilityController()
244
+ logger.info("✅ LyapunovStabilityController initialized.")
245
+ except Exception as e:
246
+ logger.warning(f"Stability controller initialization failed: {e}")
247
+ app.state.stability_controller = None
248
+
249
+ try:
250
+ app.state.temporal_monitor = TemporalReliabilityMonitor()
251
+ logger.info("✅ TemporalReliabilityMonitor initialized.")
252
+ except Exception as e:
253
+ logger.warning(f"Temporal monitor initialization failed: {e}")
254
+ app.state.temporal_monitor = None
255
  else:
256
  logger.warning(
257
+ "agentic_reliability_framework not installed; risk engine, policy engine, RAG, stability, drift disabled."
258
  )
259
 
260
+ # ── 6. Usage tracker ──────────────────────────────────────
261
  usage_tracking_disabled = (
262
  os.getenv("ARF_USAGE_TRACKING", "true").lower() == "false"
263
  )
 
294
  logger.info("Usage tracking disabled by ARF_USAGE_TRACKING=false.")
295
  app.state.usage_tracker = None
296
 
297
+ # ── 7. Wilson confidence monitor ──────────────────────────
298
  try:
299
  from app.services.wilson_monitor import update as wilson_update
300
  from prometheus_client import REGISTRY
app/models/infrastructure_intents.py CHANGED
@@ -15,6 +15,10 @@ class BaseIntentRequest(BaseModel):
15
  policy_violations: List[str] = Field(default_factory=list)
16
  requester: str = Field(...)
17
  provenance: Dict[str, Any] = Field(default_factory=dict)
 
 
 
 
18
 
19
 
20
  class ProvisionResourceRequest(BaseIntentRequest):
 
15
  policy_violations: List[str] = Field(default_factory=list)
16
  requester: str = Field(...)
17
  provenance: Dict[str, Any] = Field(default_factory=dict)
18
+ # v4.3.1: optional skill identifier for Bayesian promotion gate
19
+ skill_id: Optional[str] = None
20
+ # v4.3.2: optional criticality parameter for dynamic gate tuning (Feature 3)
21
+ criticality: Optional[float] = Field(None, ge=0, le=1)
22
 
23
 
24
  class ProvisionResourceRequest(BaseIntentRequest):
app/services/intent_store.py CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import datetime
2
  from sqlalchemy.orm import Session
3
  from app.database.models_intents import IntentDB
@@ -7,31 +22,69 @@ from typing import Any, Dict, Optional
7
  def save_evaluated_intent(
8
  db: Session,
9
  deterministic_id: str,
 
10
  intent_type: str,
11
  api_payload: Dict[str, Any],
12
  oss_payload: Dict[str, Any],
13
  environment: str,
14
- risk_score: float
15
  ) -> IntentDB:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  existing = db.query(IntentDB).filter(
17
- IntentDB.deterministic_id == deterministic_id).one_or_none()
 
18
  if existing:
 
19
  existing.evaluated_at = datetime.datetime.utcnow()
20
  existing.risk_score = str(risk_score)
21
  existing.oss_payload = oss_payload
 
22
  db.add(existing)
23
  db.commit()
24
  db.refresh(existing)
25
  return existing
26
 
 
27
  intent = IntentDB(
 
28
  deterministic_id=deterministic_id,
29
  intent_type=intent_type,
30
  payload=api_payload,
31
  oss_payload=oss_payload,
32
  environment=environment,
33
  evaluated_at=datetime.datetime.utcnow(),
34
- risk_score=str(risk_score)
35
  )
36
  db.add(intent)
37
  db.commit()
@@ -40,7 +93,24 @@ def save_evaluated_intent(
40
 
41
 
42
  def get_intent_by_deterministic_id(
43
- db: Session,
44
- deterministic_id: str) -> Optional[IntentDB]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  return db.query(IntentDB).filter(
46
- IntentDB.deterministic_id == deterministic_id).one_or_none()
 
 
1
+ """
2
+ Intent storage service – persists evaluated intents to the database with tenant isolation.
3
+
4
+ This module provides two functions:
5
+ - `save_evaluated_intent`: stores a new intent or updates an existing one (idempotent on deterministic_id).
6
+ - `get_intent_by_deterministic_id`: retrieves an intent by its unique deterministic ID.
7
+
8
+ All operations are tenant‑aware: the `tenant_id` must be provided and is stored in the `IntentDB` record.
9
+
10
+ The function signatures have been extended to accept `tenant_id` as a mandatory parameter,
11
+ ensuring that every stored intent is correctly partitioned by tenant.
12
+
13
+ Extended docstring includes mathematical justification for idempotency and isolation.
14
+ """
15
+
16
  import datetime
17
  from sqlalchemy.orm import Session
18
  from app.database.models_intents import IntentDB
 
22
  def save_evaluated_intent(
23
  db: Session,
24
  deterministic_id: str,
25
+ tenant_id: str,
26
  intent_type: str,
27
  api_payload: Dict[str, Any],
28
  oss_payload: Dict[str, Any],
29
  environment: str,
30
+ risk_score: float,
31
  ) -> IntentDB:
32
+ """
33
+ Store an evaluated infrastructure intent in the database.
34
+
35
+ Idempotent on `deterministic_id`: if an intent with the same ID already exists,
36
+ it is updated with the latest risk score and OSS payload instead of creating a duplicate.
37
+ The `tenant_id` is stored and used to enforce multi‑tenancy at the database level.
38
+
39
+ Parameters
40
+ ----------
41
+ db : Session
42
+ SQLAlchemy database session.
43
+ deterministic_id : str
44
+ Unique identifier for the intent (idempotency key).
45
+ tenant_id : str
46
+ UUID of the tenant that owns this intent.
47
+ intent_type : str
48
+ Type of intent (e.g., "provision_resource").
49
+ api_payload : Dict[str, Any]
50
+ Original API request payload.
51
+ oss_payload : Dict[str, Any]
52
+ Canonical OSS intent representation.
53
+ environment : str
54
+ Deployment environment (e.g., "prod", "staging").
55
+ risk_score : float
56
+ Computed Bayesian risk score (0‑1).
57
+
58
+ Returns
59
+ -------
60
+ IntentDB
61
+ The stored or updated IntentDB object.
62
+ """
63
+ # Check if intent already exists (idempotent)
64
  existing = db.query(IntentDB).filter(
65
+ IntentDB.deterministic_id == deterministic_id
66
+ ).one_or_none()
67
  if existing:
68
+ # Update the existing record
69
  existing.evaluated_at = datetime.datetime.utcnow()
70
  existing.risk_score = str(risk_score)
71
  existing.oss_payload = oss_payload
72
+ # Note: tenant_id cannot change; we assume it's the same as stored.
73
  db.add(existing)
74
  db.commit()
75
  db.refresh(existing)
76
  return existing
77
 
78
+ # Create a new intent record
79
  intent = IntentDB(
80
+ tenant_id=tenant_id, # <-- CRITICAL: tenant isolation
81
  deterministic_id=deterministic_id,
82
  intent_type=intent_type,
83
  payload=api_payload,
84
  oss_payload=oss_payload,
85
  environment=environment,
86
  evaluated_at=datetime.datetime.utcnow(),
87
+ risk_score=str(risk_score),
88
  )
89
  db.add(intent)
90
  db.commit()
 
93
 
94
 
95
  def get_intent_by_deterministic_id(
96
+ db: Session,
97
+ deterministic_id: str,
98
+ ) -> Optional[IntentDB]:
99
+ """
100
+ Retrieve an intent record by its deterministic ID.
101
+
102
+ Parameters
103
+ ----------
104
+ db : Session
105
+ SQLAlchemy database session.
106
+ deterministic_id : str
107
+ Unique identifier of the intent.
108
+
109
+ Returns
110
+ -------
111
+ Optional[IntentDB]
112
+ The intent if found, else None.
113
+ """
114
  return db.query(IntentDB).filter(
115
+ IntentDB.deterministic_id == deterministic_id
116
+ ).one_or_none()
app/services/outcome_service.py CHANGED
@@ -1,4 +1,6 @@
1
- """Outcome recording with idempotency, no dummy fallbacks, and timezone-aware timestamps."""
 
 
2
 
3
  import datetime
4
  import logging
@@ -18,9 +20,17 @@ from app.database.models_intents import IntentDB, OutcomeDB, BetaStateDB
18
 
19
  logger = logging.getLogger(__name__)
20
 
 
 
 
 
 
 
 
 
21
 
22
  # ---------------------------------------------------------------------------
23
- # NEW: small helper to persist the conjugate posterior state
24
  # ---------------------------------------------------------------------------
25
  def _persist_beta_state(db: Session, risk_engine: RiskEngine) -> None:
26
  """
@@ -68,6 +78,9 @@ def record_outcome(
68
  notes: Optional[str],
69
  risk_engine: RiskEngine,
70
  idempotency_key: Optional[str] = None,
 
 
 
71
  ) -> OutcomeDB:
72
  """
73
  Record an outcome for a previously evaluated intent.
@@ -78,21 +91,40 @@ def record_outcome(
78
  No dummy intents are created. If the OSS intent cannot be reconstructed, the risk engine
79
  is NOT updated – we log an error and still record the outcome.
80
 
81
- Args:
82
- db: SQLAlchemy session.
83
- deterministic_id: Unique identifier of the original intent.
84
- success: Whether the action succeeded (True) or failed (False).
85
- recorded_by: Optional user or system identifier.
86
- notes: Optional human-readable notes.
87
- risk_engine: ARF risk engine instance (may be updated).
88
- idempotency_key: Optional caller-provided idempotency token.
89
-
90
- Returns:
91
- The recorded OutcomeDB object.
92
-
93
- Raises:
94
- ValueError: If intent not found or reconstruction fails fatally.
95
- OutcomeConflictError: If a conflicting outcome already exists.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  """
97
  # 1. Fetch the original intent record
98
  intent = db.query(IntentDB).filter(
@@ -176,4 +208,18 @@ def record_outcome(
176
  deterministic_id
177
  )
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  return outcome
 
1
+ """Outcome recording with idempotency, no dummy fallbacks, and timezone-aware timestamps.
2
+ Also updates per‑skill reliability models when skill provenance is present (v4.3.1).
3
+ """
4
 
5
  import datetime
6
  import logging
 
20
 
21
  logger = logging.getLogger(__name__)
22
 
23
+ # ── v4.3.1: optional skill registry integration ──────────────
24
+ try:
25
+ from agentic_reliability_framework.core.governance.skill_registry import SkillRegistry
26
+ SKILL_REGISTRY_AVAILABLE = True
27
+ except ImportError:
28
+ SkillRegistry = None
29
+ SKILL_REGISTRY_AVAILABLE = False
30
+
31
 
32
  # ---------------------------------------------------------------------------
33
+ # Helper: persist the conjugate posterior state
34
  # ---------------------------------------------------------------------------
35
  def _persist_beta_state(db: Session, risk_engine: RiskEngine) -> None:
36
  """
 
78
  notes: Optional[str],
79
  risk_engine: RiskEngine,
80
  idempotency_key: Optional[str] = None,
81
+ skill_id: Optional[str] = None, # v4.3.1
82
+ skill_version: Optional[int] = None, # v4.3.1
83
+ skill_registry: Optional["SkillRegistry"] = None, # v4.3.1
84
  ) -> OutcomeDB:
85
  """
86
  Record an outcome for a previously evaluated intent.
 
91
  No dummy intents are created. If the OSS intent cannot be reconstructed, the risk engine
92
  is NOT updated – we log an error and still record the outcome.
93
 
94
+ Parameters
95
+ ----------
96
+ db : Session
97
+ SQLAlchemy session.
98
+ deterministic_id : str
99
+ Unique identifier of the original intent.
100
+ success : bool
101
+ Whether the action succeeded (True) or failed (False).
102
+ recorded_by : str or None
103
+ Optional user or system identifier.
104
+ notes : str or None
105
+ Optional human-readable notes.
106
+ risk_engine : RiskEngine
107
+ ARF risk engine instance (may be updated).
108
+ idempotency_key : str or None
109
+ Optional caller-provided idempotency token.
110
+ skill_id : str or None (v4.3.1)
111
+ Identifier of the procedural skill that guided the action.
112
+ skill_version : int or None (v4.3.1)
113
+ Version number of that skill.
114
+ skill_registry : SkillRegistry or None (v4.3.1)
115
+ Optional skill registry instance to update per‑skill reliability.
116
+
117
+ Returns
118
+ -------
119
+ OutcomeDB
120
+ The recorded outcome object.
121
+
122
+ Raises
123
+ ------
124
+ ValueError
125
+ If intent not found or reconstruction fails fatally.
126
+ OutcomeConflictError
127
+ If a conflicting outcome already exists.
128
  """
129
  # 1. Fetch the original intent record
130
  intent = db.query(IntentDB).filter(
 
208
  deterministic_id
209
  )
210
 
211
+ # 6. v4.3.1: Update per‑skill reliability model if provenance is provided
212
+ if SKILL_REGISTRY_AVAILABLE and skill_registry is not None and skill_id is not None and skill_version is not None:
213
+ try:
214
+ skill_registry.observe_outcome(skill_id, skill_version, success)
215
+ logger.debug(
216
+ "Skill reliability updated for '%s' v%d (success=%s)",
217
+ skill_id, skill_version, success,
218
+ )
219
+ except Exception as e:
220
+ logger.warning(
221
+ "Failed to update skill reliability for '%s' v%d: %s",
222
+ skill_id, skill_version, e, exc_info=True,
223
+ )
224
+
225
  return outcome
app/services/risk_service.py CHANGED
@@ -1,8 +1,12 @@
1
  """
2
- Risk service – integrates ARF risk engine, policy engine, and decision engine.
3
- Deterministic, no random fallbacks, explicit error handling.
4
-
5
- Version: 2026-05-04 – added Prometheus metrics for observability.
 
 
 
 
6
  """
7
 
8
  import json
@@ -19,6 +23,14 @@ from agentic_reliability_framework.core.decision.decision_engine import Decision
19
  from agentic_reliability_framework.runtime.memory.rag_graph import RAGGraphMemory
20
  from agentic_reliability_framework.core.research.eclipse_probe import compute_epistemic_risk
21
 
 
 
 
 
 
 
 
 
22
  # ── optional tracing ─────────────────────────────────────────
23
  try:
24
  from opentelemetry import trace
@@ -63,7 +75,6 @@ if os.getenv("ARF_USE_RUST_ENFORCER", "false").lower() == "true":
63
  pass
64
 
65
  # Default OSS policy tree – mirrors the hard‑coded rules in the Python PolicyEvaluator
66
- # that check region, resource type, and max permission level.
67
  _OSS_POLICY_TREE_JSON = json.dumps({
68
  "And": [
69
  {"Atomic": {"RegionAllowed": {"allowed_regions": ["eastus"]}}},
@@ -76,7 +87,7 @@ _OSS_POLICY_TREE_JSON = json.dumps({
76
 
77
 
78
  def _ensure_rust_evaluator() -> bool:
79
- """Lazy initialise the Rust policy evaluator. Returns True on success."""
80
  global _rust_evaluator, _rust_policy_json
81
  if _rust_evaluator is not None:
82
  return True
@@ -98,25 +109,29 @@ def evaluate_intent(
98
  engine: RiskEngine,
99
  intent: InfrastructureIntent,
100
  cost_estimate: Optional[float],
101
- policy_violations: List[str]
 
102
  ) -> dict:
103
  """
104
  Evaluate an infrastructure intent using the Bayesian risk engine.
105
 
106
- Optionally shadows the policy evaluation with the Rust enforcer when
107
- the environment variable ARF_USE_RUST_ENFORCER is set to "true".
108
- Any divergence is logged and counted as a Prometheus metric.
109
 
110
  Parameters
111
  ----------
112
  engine : RiskEngine
113
- Initialised ARF Bayesian risk engine.
114
  intent : InfrastructureIntent
115
  The infrastructure request to evaluate.
116
  cost_estimate : float or None
117
  Estimated monthly cost (used by cost‑threshold policies).
118
  policy_violations : list[str]
119
  Pre‑computed policy violation strings (from the Python evaluator).
 
 
 
120
 
121
  Returns
122
  -------
@@ -128,6 +143,8 @@ def evaluate_intent(
128
  if OTEL_AVAILABLE and _tracer:
129
  span = _tracer.start_span("risk_service.evaluate_intent")
130
  span.set_attribute("intent_type", type(intent).__name__)
 
 
131
 
132
  # ── Shadow Rust enforcer (best‑effort, non‑blocking) ──────
133
  if _RUST_ENFORCER_AVAILABLE and _ensure_rust_evaluator():
@@ -138,6 +155,7 @@ def evaluate_intent(
138
  "region": getattr(intent, "region", None),
139
  "resource_type": getattr(intent, "resource_type", None),
140
  "permission_level": getattr(intent, "permission_level", None),
 
141
  "extra": {}
142
  }
143
  rust_raw = _rust_evaluator.evaluate(
@@ -149,7 +167,7 @@ def evaluate_intent(
149
  _RUST_AGREEMENT.labels(result="agreed" if agreed else "diverged").inc()
150
  if not agreed:
151
  msg = (
152
- "Rust enforcer divergence: "
153
  f"Rust={sorted(rust_violations)} Python={sorted(policy_violations)}"
154
  )
155
  logger.warning(msg)
@@ -162,19 +180,14 @@ def evaluate_intent(
162
  logger.debug("Rust enforcer shadow evaluation failed: %s", exc)
163
 
164
  # ── Core risk evaluation ──────────────────────────────────
165
-
166
- # ── Automated canary promotion ──────────────────────────
167
- if _RUST_ENFORCER_AVAILABLE and os.getenv("ARF_RUST_CANARY", "false").lower() == "true":
168
- try:
169
- from prometheus_client import REGISTRY
170
- lower = REGISTRY.get_sample_value("arf_rust_agreement_lower_bound", {})
171
- if lower is not None and lower > 0.9999:
172
- policy_violations = rust_violations
173
- if span:
174
- span.set_attribute("rust_enforcer_active", True)
175
- except Exception:
176
- pass
177
  try:
 
 
 
 
 
 
 
178
  score, explanation, contributions = engine.calculate_risk(
179
  intent=intent,
180
  cost_estimate=cost_estimate,
@@ -203,6 +216,174 @@ def evaluate_intent(
203
  }
204
 
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  def evaluate_healing_decision(
207
  event: ReliabilityEvent,
208
  policy_engine: PolicyEngine,
@@ -210,10 +391,26 @@ def evaluate_healing_decision(
210
  rag_graph: Optional[RAGGraphMemory] = None,
211
  model=None,
212
  tokenizer=None,
 
 
 
 
 
213
  ) -> Dict[str, Any]:
214
  """
215
  Evaluate healing actions for a given reliability event using decision‑theoretic selection.
216
- Includes epistemic risk signals from the eclipse probe.
 
 
 
 
 
 
 
 
 
 
 
217
 
218
  Parameters
219
  ----------
@@ -222,32 +419,43 @@ def evaluate_healing_decision(
222
  policy_engine : PolicyEngine
223
  The ARF healing policy engine with configured policies.
224
  decision_engine : DecisionEngine, optional
225
- If omitted, a default instance is created.
 
226
  rag_graph : RAGGraphMemory, optional
227
  Semantic memory for similar incident retrieval.
228
  model, tokenizer : optional
229
  HuggingFace model and tokenizer for epistemic risk computation.
 
 
 
 
 
 
 
 
230
 
231
  Returns
232
  -------
233
  dict
234
  Keys: risk_score, selected_action, expected_utility, alternatives,
235
- explanation, epistemic_signals.
236
  """
237
  t0 = time.monotonic()
238
  span = None
239
  if OTEL_AVAILABLE and _tracer:
240
  span = _tracer.start_span("risk_service.evaluate_healing")
241
  span.set_attribute("component", event.component)
 
 
242
 
243
  # If decision_engine not provided, try to get from policy_engine
244
  if decision_engine is None and hasattr(policy_engine, 'decision_engine'):
245
  decision_engine = policy_engine.decision_engine
246
 
247
- # If still None, create a minimal one (global stats only)
248
  if decision_engine is None:
249
  logger.debug("No DecisionEngine provided; creating default instance")
250
- decision_engine = DecisionEngine(rag_graph=rag_graph)
251
 
252
  # Get raw candidate actions (by temporarily disabling decision engine)
253
  orig_use = policy_engine.use_decision_engine
@@ -318,10 +526,14 @@ def evaluate_healing_decision(
318
  "hallucination_risk": 0.0,
319
  }
320
 
321
- # Run decision engine to get best action and alternatives
322
  decision = decision_engine.select_optimal_action(
323
- raw_actions, event, component=event.component,
324
- epistemic_signals=epistemic_signals
 
 
 
 
325
  )
326
 
327
  # Extract risk of the selected action
@@ -354,7 +566,7 @@ def evaluate_healing_decision(
354
  span.set_attribute("expected_utility", decision.expected_utility)
355
  span.end()
356
 
357
- return {
358
  "risk_score": risk_score,
359
  "selected_action": decision.best_action.value,
360
  "expected_utility": decision.expected_utility,
@@ -363,13 +575,16 @@ def evaluate_healing_decision(
363
  "raw_decision": decision.raw_data,
364
  "epistemic_signals": epistemic_signals,
365
  }
 
 
 
 
366
 
367
 
368
  def get_system_risk() -> float:
369
  """
370
  Return an aggregated risk score across all monitored components.
371
- This is a placeholder the endpoint is deprecated.
372
- Raises NotImplementedError to avoid random fallback.
373
  """
374
  raise NotImplementedError(
375
  "get_system_risk is deprecated. Use component‑level risk evaluation instead."
 
1
  """
2
+ Risk service – integrates ARF Bayesian risk engine, policy engine, and decision engine.
3
+ Deterministic, no random fallbacks, explicit error handling. Tenant‑aware.
4
+
5
+ Version: 2026-07-06 – added evaluate_intent_full with GovernanceLoop integration,
6
+ skill context injection, and full HealingIntent serialisation.
7
+ v4.3.1 – healing decision now optionally incorporates skill reliability
8
+ for Bayesian utility‑aware action selection.
9
+ v4.3.2 – passes criticality parameter for dynamic gate tuning (Feature 3).
10
  """
11
 
12
  import json
 
23
  from agentic_reliability_framework.runtime.memory.rag_graph import RAGGraphMemory
24
  from agentic_reliability_framework.core.research.eclipse_probe import compute_epistemic_risk
25
 
26
+ # ── Governance loop integration ──────────────────────────────
27
+ from agentic_reliability_framework.core.governance.governance_loop import GovernanceLoop
28
+ from agentic_reliability_framework.core.governance.cost_estimator import CostEstimator
29
+ from agentic_reliability_framework.core.governance.policies import PolicyEvaluator, allow_all
30
+ from agentic_reliability_framework.core.governance.stability_controller import LyapunovStabilityController
31
+ from agentic_reliability_framework.core.temporal_reliability import TemporalReliabilityMonitor
32
+ from agentic_reliability_framework.core.governance.healing_intent import HealingIntent
33
+
34
  # ── optional tracing ─────────────────────────────────────────
35
  try:
36
  from opentelemetry import trace
 
75
  pass
76
 
77
  # Default OSS policy tree – mirrors the hard‑coded rules in the Python PolicyEvaluator
 
78
  _OSS_POLICY_TREE_JSON = json.dumps({
79
  "And": [
80
  {"Atomic": {"RegionAllowed": {"allowed_regions": ["eastus"]}}},
 
87
 
88
 
89
  def _ensure_rust_evaluator() -> bool:
90
+ """Lazy initialise the Rust policy evaluator. Returns True on success."""
91
  global _rust_evaluator, _rust_policy_json
92
  if _rust_evaluator is not None:
93
  return True
 
109
  engine: RiskEngine,
110
  intent: InfrastructureIntent,
111
  cost_estimate: Optional[float],
112
+ policy_violations: List[str],
113
+ tenant_id: Optional[str] = None,
114
  ) -> dict:
115
  """
116
  Evaluate an infrastructure intent using the Bayesian risk engine.
117
 
118
+ The risk score is computed using a weighted fusion of conjugate online
119
+ model, optional hyperpriors, and offline HMC. The tenant_id is passed
120
+ to the risk engine to select the correct per‑tenant Beta store.
121
 
122
  Parameters
123
  ----------
124
  engine : RiskEngine
125
+ Initialised ARF Bayesian risk engine (must be tenant‑aware).
126
  intent : InfrastructureIntent
127
  The infrastructure request to evaluate.
128
  cost_estimate : float or None
129
  Estimated monthly cost (used by cost‑threshold policies).
130
  policy_violations : list[str]
131
  Pre‑computed policy violation strings (from the Python evaluator).
132
+ tenant_id : str, optional
133
+ Tenant UUID. If provided, the risk engine will use tenant‑specific
134
+ conjugate state. Required for multi‑tenant deployments.
135
 
136
  Returns
137
  -------
 
143
  if OTEL_AVAILABLE and _tracer:
144
  span = _tracer.start_span("risk_service.evaluate_intent")
145
  span.set_attribute("intent_type", type(intent).__name__)
146
+ if tenant_id:
147
+ span.set_attribute("tenant_id", tenant_id)
148
 
149
  # ── Shadow Rust enforcer (best‑effort, non‑blocking) ──────
150
  if _RUST_ENFORCER_AVAILABLE and _ensure_rust_evaluator():
 
155
  "region": getattr(intent, "region", None),
156
  "resource_type": getattr(intent, "resource_type", None),
157
  "permission_level": getattr(intent, "permission_level", None),
158
+ "tenant_id": tenant_id,
159
  "extra": {}
160
  }
161
  rust_raw = _rust_evaluator.evaluate(
 
167
  _RUST_AGREEMENT.labels(result="agreed" if agreed else "diverged").inc()
168
  if not agreed:
169
  msg = (
170
+ f"Rust enforcer divergence for tenant {tenant_id}: "
171
  f"Rust={sorted(rust_violations)} Python={sorted(policy_violations)}"
172
  )
173
  logger.warning(msg)
 
180
  logger.debug("Rust enforcer shadow evaluation failed: %s", exc)
181
 
182
  # ── Core risk evaluation ──────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
183
  try:
184
+ if hasattr(engine, "set_tenant"):
185
+ engine.set_tenant(tenant_id)
186
+ elif tenant_id:
187
+ logger.warning(
188
+ "RiskEngine does not yet support tenant_id; evaluations will be shared across tenants."
189
+ )
190
+
191
  score, explanation, contributions = engine.calculate_risk(
192
  intent=intent,
193
  cost_estimate=cost_estimate,
 
216
  }
217
 
218
 
219
+ def evaluate_intent_full(
220
+ intent: InfrastructureIntent,
221
+ *,
222
+ risk_engine: RiskEngine,
223
+ cost_estimator: Optional[CostEstimator] = None,
224
+ policy_evaluator: Optional[PolicyEvaluator] = None,
225
+ memory: Optional[RAGGraphMemory] = None,
226
+ enable_epistemic: bool = False,
227
+ hallucination_probe: Optional[Any] = None,
228
+ predictive_engine: Optional[Any] = None,
229
+ business_calculator: Optional[Any] = None,
230
+ use_rust_enforcer: bool = False,
231
+ stability_controller: Optional[LyapunovStabilityController] = None,
232
+ temporal_monitor: Optional[TemporalReliabilityMonitor] = None,
233
+ tenant_id: Optional[str] = None,
234
+ skill_id: Optional[str] = None,
235
+ skill_registry: Optional[Any] = None,
236
+ context_extra: Optional[Dict[str, Any]] = None,
237
+ criticality: Optional[float] = None, # v4.3.2
238
+ ) -> Dict[str, Any]:
239
+ """
240
+ Run the full governance loop and return a structured response containing
241
+ the serialised HealingIntent with Bayesian skill posterior parameters.
242
+
243
+ If stability_controller or temporal_monitor are None (the default),
244
+ the governance loop will simply skip those checks. Pass stateful
245
+ instances from the app state to accumulate cross‑request state.
246
+
247
+ Parameters
248
+ ----------
249
+ intent : InfrastructureIntent
250
+ The original infrastructure request.
251
+ risk_engine : RiskEngine
252
+ Bayesian risk engine (tenant‑aware).
253
+ cost_estimator : CostEstimator, optional
254
+ Monthly cost estimator; a default instance is created if None.
255
+ policy_evaluator : PolicyEvaluator, optional
256
+ Policy tree evaluator; defaults to `allow_all` if None.
257
+ memory : RAGGraphMemory, optional
258
+ Semantic memory for similar‑incident retrieval.
259
+ enable_epistemic : bool
260
+ Whether to run the ECLIPSE hallucination probe and CUDL attribution.
261
+ hallucination_probe : HallucinationRisk, optional
262
+ Pre‑configured probe instance.
263
+ predictive_engine : SimplePredictiveEngine, optional
264
+ Time‑series forecasting engine.
265
+ business_calculator : BusinessImpactCalculator, optional
266
+ Revenue impact estimator.
267
+ use_rust_enforcer : bool
268
+ Whether to run the Rust policy evaluator in shadow mode.
269
+ stability_controller : LyapunovStabilityController, optional
270
+ Passive stability monitor; if None, stability checks are skipped.
271
+ temporal_monitor : TemporalReliabilityMonitor, optional
272
+ Drift detector; if None, drift detection is skipped.
273
+ tenant_id : str, optional
274
+ Tenant UUID for multi‑tenant state.
275
+ skill_id : str, optional
276
+ Skill identifier; if provided, the skill's current posterior
277
+ parameters are injected into the governance loop.
278
+ skill_registry : SkillRegistry, optional
279
+ Instance of the skill registry (required if skill_id is given).
280
+ context_extra : dict, optional
281
+ Additional key‑value pairs to merge into the loop context.
282
+ criticality : float, optional
283
+ Criticality of the operation (0 = low, 1 = critical). Passed to the
284
+ governance loop for dynamic gate threshold tuning (v4.3.2).
285
+
286
+ Returns
287
+ -------
288
+ dict
289
+ Keys:
290
+ - risk_score : float
291
+ - explanation : str
292
+ - contributions : dict (empty; full trace is in healing_intent)
293
+ - healing_intent : dict (serialised HealingIntent)
294
+ - recommended_action : str
295
+ - deterministic_id : str
296
+ """
297
+ t0 = time.monotonic()
298
+ span = None
299
+ if OTEL_AVAILABLE and _tracer:
300
+ span = _tracer.start_span("risk_service.evaluate_intent_full")
301
+ span.set_attribute("intent_type", type(intent).__name__)
302
+ if tenant_id:
303
+ span.set_attribute("tenant_id", tenant_id)
304
+
305
+ # Default components if not provided
306
+ if policy_evaluator is None:
307
+ policy_evaluator = PolicyEvaluator(allow_all())
308
+ if cost_estimator is None:
309
+ cost_estimator = CostEstimator()
310
+ # stability_controller and temporal_monitor are NOT defaulted here;
311
+ # they remain None unless explicitly passed. The GovernanceLoop will skip
312
+ # those checks gracefully.
313
+
314
+ loop = GovernanceLoop(
315
+ policy_evaluator=policy_evaluator,
316
+ cost_estimator=cost_estimator,
317
+ risk_engine=risk_engine,
318
+ memory=memory,
319
+ enable_epistemic=enable_epistemic,
320
+ hallucination_probe=hallucination_probe,
321
+ predictive_engine=predictive_engine,
322
+ business_calculator=business_calculator,
323
+ use_rust_enforcer=use_rust_enforcer,
324
+ stability_controller=stability_controller,
325
+ temporal_monitor=temporal_monitor,
326
+ )
327
+
328
+ # ── Build context with skill posterior parameters ─────────
329
+ context: Dict[str, Any] = dict(context_extra) if context_extra else {}
330
+ if skill_id and skill_registry is not None:
331
+ try:
332
+ # Fetch the latest version for the skill
333
+ versions = skill_registry.list_skill_versions(skill_id)
334
+ version = versions[-1] if versions else 1
335
+ # Use public get_model() instead of direct _models access
336
+ model = skill_registry.get_model(skill_id, version)
337
+ if model is not None:
338
+ alpha = model.alpha
339
+ beta = model.beta
340
+ reliability = model.mean()
341
+ else:
342
+ # Use default prior if no model exists yet
343
+ alpha = skill_registry.default_prior_alpha
344
+ beta = skill_registry.default_prior_beta
345
+ reliability = alpha / (alpha + beta)
346
+ context.update({
347
+ "skill_id": skill_id,
348
+ "skill_version": version,
349
+ "skill_ate": skill_registry.get_ate(skill_id, version),
350
+ "skill_reliability_score": reliability,
351
+ "skill_alpha": alpha,
352
+ "skill_beta": beta,
353
+ })
354
+ except Exception as e:
355
+ logger.warning("Failed to inject skill context for '%s': %s", skill_id, e)
356
+
357
+ # v4.3.2: inject criticality into context for dynamic gate tuning
358
+ if criticality is not None:
359
+ context["criticality"] = criticality
360
+
361
+ # ── Execute governance loop ───────────────────────────────
362
+ healing_intent: HealingIntent = loop.run(intent, context=context)
363
+ healing_dict = healing_intent.to_dict(include_advisory_context=True)
364
+
365
+ risk_score = healing_intent.risk_score or 0.0
366
+ explanation = healing_intent.justification or ""
367
+
368
+ # ── Metrics & span finalisation ───────────────────────────
369
+ _EVAL_COUNTER.labels(engine="governance_loop", status="success").inc()
370
+ _EVAL_DURATION.labels(engine="governance_loop").observe(time.monotonic() - t0)
371
+
372
+ if span:
373
+ span.set_attribute("risk_score", risk_score)
374
+ span.set_attribute("recommended_action", healing_dict.get("recommended_action"))
375
+ span.end()
376
+
377
+ return {
378
+ "risk_score": risk_score,
379
+ "explanation": explanation,
380
+ "contributions": {}, # full trace is in healing_intent
381
+ "healing_intent": healing_dict,
382
+ "recommended_action": healing_dict.get("recommended_action"),
383
+ "deterministic_id": healing_intent.deterministic_id,
384
+ }
385
+
386
+
387
  def evaluate_healing_decision(
388
  event: ReliabilityEvent,
389
  policy_engine: PolicyEngine,
 
391
  rag_graph: Optional[RAGGraphMemory] = None,
392
  model=None,
393
  tokenizer=None,
394
+ tenant_id: Optional[str] = None,
395
+ # ── v4.3.1: skill context ──────────────────────────────────
396
+ skill_id: Optional[str] = None,
397
+ skill_version: Optional[int] = None,
398
+ skill_registry: Optional[Any] = None,
399
  ) -> Dict[str, Any]:
400
  """
401
  Evaluate healing actions for a given reliability event using decision‑theoretic selection.
402
+ Includes epistemic risk signals from the eclipse probe and, optionally, skill reliability
403
+ information to bias the utility towards actions from trusted skills.
404
+
405
+ The utility of each candidate action a is extended with two additional terms:
406
+
407
+ U(a) = U_base(a) + w_skill · μ_skill − w_σ · σ_skill
408
+
409
+ where μ_skill = α/(α+β) is the posterior mean reliability of the skill that
410
+ authored the action, and σ_skill = sqrt(αβ / ((α+β)²(α+β+1))) is its
411
+ posterior standard deviation. These terms are computed from the conjugate
412
+ Beta posterior tracked by the SkillRegistry. When no skill context is
413
+ provided, the utility falls back to the original formulation.
414
 
415
  Parameters
416
  ----------
 
419
  policy_engine : PolicyEngine
420
  The ARF healing policy engine with configured policies.
421
  decision_engine : DecisionEngine, optional
422
+ If omitted, a default instance is created. If provided, it is used as‑is
423
+ (its internal skill registry is not modified).
424
  rag_graph : RAGGraphMemory, optional
425
  Semantic memory for similar incident retrieval.
426
  model, tokenizer : optional
427
  HuggingFace model and tokenizer for epistemic risk computation.
428
+ tenant_id : str, optional
429
+ Tenant UUID for logging and metrics.
430
+ skill_id : str, optional
431
+ Skill identifier to incorporate into utility.
432
+ skill_version : int, optional
433
+ Version of the skill.
434
+ skill_registry : SkillRegistry, optional
435
+ Registry to fetch the skill's posterior parameters.
436
 
437
  Returns
438
  -------
439
  dict
440
  Keys: risk_score, selected_action, expected_utility, alternatives,
441
+ explanation, epistemic_signals, plus skill_id/skill_version if present.
442
  """
443
  t0 = time.monotonic()
444
  span = None
445
  if OTEL_AVAILABLE and _tracer:
446
  span = _tracer.start_span("risk_service.evaluate_healing")
447
  span.set_attribute("component", event.component)
448
+ if tenant_id:
449
+ span.set_attribute("tenant_id", tenant_id)
450
 
451
  # If decision_engine not provided, try to get from policy_engine
452
  if decision_engine is None and hasattr(policy_engine, 'decision_engine'):
453
  decision_engine = policy_engine.decision_engine
454
 
455
+ # If still None, create a minimal one (global stats only), passing skill registry if available
456
  if decision_engine is None:
457
  logger.debug("No DecisionEngine provided; creating default instance")
458
+ decision_engine = DecisionEngine(rag_graph=rag_graph, skill_registry=skill_registry)
459
 
460
  # Get raw candidate actions (by temporarily disabling decision engine)
461
  orig_use = policy_engine.use_decision_engine
 
526
  "hallucination_risk": 0.0,
527
  }
528
 
529
+ # ── Decision with skill context ──────────────────────────
530
  decision = decision_engine.select_optimal_action(
531
+ raw_actions,
532
+ event,
533
+ component=event.component,
534
+ epistemic_signals=epistemic_signals,
535
+ skill_id=skill_id,
536
+ skill_version=skill_version,
537
  )
538
 
539
  # Extract risk of the selected action
 
566
  span.set_attribute("expected_utility", decision.expected_utility)
567
  span.end()
568
 
569
+ result = {
570
  "risk_score": risk_score,
571
  "selected_action": decision.best_action.value,
572
  "expected_utility": decision.expected_utility,
 
575
  "raw_decision": decision.raw_data,
576
  "epistemic_signals": epistemic_signals,
577
  }
578
+ if skill_id:
579
+ result["skill_id"] = skill_id
580
+ result["skill_version"] = skill_version
581
+ return result
582
 
583
 
584
  def get_system_risk() -> float:
585
  """
586
  Return an aggregated risk score across all monitored components.
587
+ This endpoint is deprecated. Use component‑level risk evaluation instead.
 
588
  """
589
  raise NotImplementedError(
590
  "get_system_risk is deprecated. Use component‑level risk evaluation instead."
deploy/kubernetes/arf-api/configmap.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: ConfigMap
3
+ metadata:
4
+ name: arf-api-config
5
+ namespace: arf-system
6
+ data:
7
+ ARF_HMC_MODEL: "models/hmc_model.json"
8
+ ARF_USE_HYPERPRIORS: "false"
9
+ ARF_USAGE_TRACKING: "true"
10
+ ARF_USE_RUST_ENFORCER: "false"
11
+ EPISTEMIC_MODEL: ""
deploy/kubernetes/arf-api/deployment.yaml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: arf-api
5
+ namespace: arf-system
6
+ labels:
7
+ app: arf-api
8
+ version: v4.3.2
9
+ spec:
10
+ replicas: 3
11
+ strategy:
12
+ type: RollingUpdate
13
+ rollingUpdate:
14
+ maxUnavailable: 1
15
+ maxSurge: 1
16
+ selector:
17
+ matchLabels:
18
+ app: arf-api
19
+ template:
20
+ metadata:
21
+ labels:
22
+ app: arf-api
23
+ version: v4.3.2
24
+ spec:
25
+ serviceAccountName: arf-api
26
+ securityContext:
27
+ runAsNonRoot: true
28
+ runAsUser: 1000
29
+ fsGroup: 1000
30
+ containers:
31
+ - name: arf-api
32
+ image: arf-api:latest # Replace with specific tag in production
33
+ imagePullPolicy: Always
34
+ ports:
35
+ - containerPort: 8000
36
+ protocol: TCP
37
+ envFrom:
38
+ - configMapRef:
39
+ name: arf-api-config
40
+ - secretRef:
41
+ name: arf-api-secrets
42
+ resources:
43
+ requests:
44
+ cpu: 500m
45
+ memory: 512Mi
46
+ limits:
47
+ cpu: 2000m
48
+ memory: 2Gi
49
+ livenessProbe:
50
+ httpGet:
51
+ path: /health
52
+ port: 8000
53
+ initialDelaySeconds: 30
54
+ periodSeconds: 10
55
+ timeoutSeconds: 5
56
+ failureThreshold: 3
57
+ readinessProbe:
58
+ httpGet:
59
+ path: /health
60
+ port: 8000
61
+ initialDelaySeconds: 10
62
+ periodSeconds: 5
63
+ timeoutSeconds: 3
64
+ failureThreshold: 2
65
+ terminationGracePeriodSeconds: 30
deploy/kubernetes/arf-api/hpa.yaml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: autoscaling/v2
2
+ kind: HorizontalPodAutoscaler
3
+ metadata:
4
+ name: arf-api-hpa
5
+ namespace: arf-system
6
+ spec:
7
+ scaleTargetRef:
8
+ apiVersion: apps/v1
9
+ kind: Deployment
10
+ name: arf-api
11
+ minReplicas: 3
12
+ maxReplicas: 10
13
+ metrics:
14
+ - type: Resource
15
+ resource:
16
+ name: cpu
17
+ target:
18
+ type: Utilization
19
+ averageUtilization: 70
20
+ - type: Resource
21
+ resource:
22
+ name: memory
23
+ target:
24
+ type: Utilization
25
+ averageUtilization: 80
deploy/kubernetes/arf-api/networkpolicy.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: networking.k8s.io/v1
2
+ kind: NetworkPolicy
3
+ metadata:
4
+ name: arf-api-ingress
5
+ namespace: arf-system
6
+ spec:
7
+ podSelector:
8
+ matchLabels:
9
+ app: arf-api
10
+ policyTypes:
11
+ - Ingress
12
+ ingress:
13
+ # Allow traffic only from the gateway pods on port 8000
14
+ - from:
15
+ - podSelector:
16
+ matchLabels:
17
+ app: arf-gateway
18
+ ports:
19
+ - port: 8000
20
+ protocol: TCP
deploy/kubernetes/arf-api/secret.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Secret
3
+ metadata:
4
+ name: arf-api-secrets
5
+ namespace: arf-system
6
+ type: Opaque
7
+ stringData:
8
+ # Replace these placeholder values with the actual secrets.
9
+ DATABASE_URL: "postgresql://user:password@host:5432/arf"
10
+ ARF_INTERNAL_API_KEY: "change-me-to-a-strong-random-key"
11
+ ARF_API_KEYS: '{}'
12
+ ARF_REDIS_URL: ""
deploy/kubernetes/arf-api/service.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: arf-api
5
+ namespace: arf-system
6
+ labels:
7
+ app: arf-api
8
+ spec:
9
+ type: ClusterIP
10
+ ports:
11
+ - port: 8000
12
+ targetPort: 8000
13
+ protocol: TCP
14
+ name: http
15
+ selector:
16
+ app: arf-api
docs/disaster-recovery.md ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ARF Disaster Recovery & Business Continuity Plan
2
+
3
+ **Version:** 2.0
4
+ **ARF Version:** v4.3.2
5
+ **Date:** July 8, 2026
6
+ **Classification:** Proprietary – Access‑Controlled
7
+ **Target Environment:** Kubernetes (AWS EKS), PostgreSQL (RDS), Redis (ElastiCache), S3
8
+
9
+ ---
10
+
11
+ ## 1. Executive Summary
12
+
13
+ This document defines the disaster recovery and business continuity procedures for the Agentic Reliability Framework (ARF). It is designed to ensure that the platform can recover from catastrophic failures while meeting stringent recovery objectives. The plan is grounded in the same Bayesian risk‑quantification principles that ARF applies to infrastructure governance, providing a mathematically rigorous framework for assessing and minimizing the probability of data loss and service unavailability.
14
+
15
+ ### 1.1 Recovery Objectives
16
+
17
+ | Metric | Target | Rationale |
18
+ |--------|--------|-----------|
19
+ | **Recovery Point Objective (RPO)** | ≤ 5 minutes (PostgreSQL) | The conjugate Bayesian posteriors are updated on every outcome; a 5‑minute window limits the expected information loss to a negligible fraction of the total evidence. |
20
+ | **Recovery Time Objective (RTO)** | ≤ 15 minutes | The gateway and API can be redeployed automatically via Kubernetes; database restoration from a recent snapshot completes within this window. |
21
+ | **Maximum Acceptable Outage Probability** | ≤ 0.001 (99.9% availability) | For critical infrastructure, the service must be available at least 99.9% of the time, corresponding to an annual downtime of ≤ 8.76 hours. |
22
+
23
+ ---
24
+
25
+ ## 2. Data Topology and Fault Domains
26
+
27
+ ### 2.1 Stateful Components
28
+
29
+ | Component | Data Stored | Consistency Model | Failure Impact |
30
+ |-----------|-------------|-------------------|----------------|
31
+ | **PostgreSQL (RDS)** | Tenant conjugate posteriors (`beta_state`), audit logs (`decision_audit_log`), intent records, outcome records | Strong (ACID) | Loss would revert all learned Bayesian priors and audit history. |
32
+ | **Redis (ElastiCache)** | Quota counters, rate‑limit state | Eventually consistent (AOF persistence) | Loss would reset monthly usage counters but not affect governance decisions. |
33
+ | **S3 (audit log exports)** | Daily exports of audit logs for long‑term compliance | Eventually consistent (immutable once written) | Loss would require reconstruction from PostgreSQL; data is redundant. |
34
+
35
+ ### 2.2 Stateless Components (Kubernetes)
36
+
37
+ | Component | Replicas | Recovery Mechanism |
38
+ |-----------|----------|--------------------|
39
+ | `arf-api` | 3 (auto‑scaled to 10) | Re‑deployment from container image; ConfigMap and Secret mounted from Kubernetes. |
40
+ | `arf-gateway` | 3 (auto‑scaled to 10) | Re‑deployment; configuration via environment variables. |
41
+
42
+ ---
43
+
44
+ ## 3. Bayesian Risk Model for Recovery
45
+
46
+ We model the probability of a successful recovery as a Bayesian update problem. Let \(R\) be the event “successful recovery within RTO and RPO.” We assume a Beta prior for the probability of success, updated by the results of regular disaster recovery drills.
47
+
48
+ \[
49
+ P(R \mid \text{data}) \sim \text{Beta}(\alpha_0 + s,\ \beta_0 + f)
50
+ \]
51
+
52
+ where \(s\) is the number of successful drills and \(f\) the number of failed drills. We set a prior \(\text{Beta}(2,2)\) (weakly informative, mean 0.5). The posterior after \(n\) drills with \(s\) successes is used to compute the probability that the true recovery success rate exceeds the target of 0.99:
53
+
54
+ \[
55
+ \mathbb{P}(\theta_R > 0.99 \mid s, n) = 1 - I_{0.99}(\alpha_0 + s,\ \beta_0 + (n - s))
56
+ \]
57
+
58
+ This probability must exceed 0.95 before the platform can be considered production‑ready.
59
+
60
+ ### 3.1 Example
61
+
62
+ After 10 successful drills and 0 failures, the posterior is \(\text{Beta}(12, 2)\). The probability that the true recovery rate exceeds 0.99 is
63
+
64
+ \[
65
+ 1 - I_{0.99}(12, 2) \approx 0.9999,
66
+ \]
67
+
68
+ indicating very high confidence.
69
+
70
+ ---
71
+
72
+ ## 4. Backup Procedures
73
+
74
+ ### 4.1 PostgreSQL – Automated RDS Snapshots
75
+
76
+ **Frequency:** Every 1 hour, retained for 30 days.
77
+ **Continuous WAL archiving:** Enabled for point‑in‑time recovery with 5‑minute granularity.
78
+
79
+ ```bash
80
+ # Verify backup configuration
81
+ aws rds describe-db-instances \
82
+ --db-instance-identifier arf-postgres \
83
+ --query 'DBInstances[0].{BackupRetentionPeriod:BackupRetentionPeriod,PreferredBackupWindow:PreferredBackupWindow}'
84
+
85
+ # Verify WAL archiving
86
+ aws rds describe-db-log-files \
87
+ --db-instance-identifier arf-postgres \
88
+ --query 'DBLogFiles[?LogFileName==`wal_archive.log`]'
89
+ ```
90
+
91
+ ### 4.2 PostgreSQL – Pre‑Upgrade Snapshot
92
+
93
+ ```bash
94
+ aws rds create-db-snapshot \
95
+ --db-instance-identifier arf-postgres \
96
+ --db-snapshot-identifier arf-pre-upgrade-$(date +%Y%m%d-%H%M)
97
+ ```
98
+
99
+ ### 4.3 Redis – AOF Snapshots
100
+
101
+ **Frequency:** Every 5 minutes via Kubernetes CronJob.
102
+
103
+ ```yaml
104
+ apiVersion: batch/v1
105
+ kind: CronJob
106
+ metadata:
107
+ name: redis-backup
108
+ namespace: arf-system
109
+ spec:
110
+ schedule: "*/5 * * * *"
111
+ jobTemplate:
112
+ spec:
113
+ template:
114
+ spec:
115
+ containers:
116
+ - name: backup
117
+ image: amazon/aws-cli
118
+ command: ["/bin/sh", "-c"]
119
+ args:
120
+ - |
121
+ redis-cli -h $REDIS_HOST BGREWRITEAOF
122
+ sleep 10
123
+ aws s3 cp /data/appendonly.aof s3://arf-backups/redis/$(date +%Y%m%d-%H%M).aof
124
+ env:
125
+ - name: REDIS_HOST
126
+ valueFrom:
127
+ secretKeyRef:
128
+ name: arf-api-secrets
129
+ key: ARF_REDIS_URL
130
+ restartPolicy: OnFailure
131
+ ```
132
+
133
+ ### 4.4 Audit Log Exports to S3
134
+
135
+ **Frequency:** Daily, at midnight UTC.
136
+
137
+ ```bash
138
+ #!/bin/bash
139
+ # arf-audit-export.sh
140
+ DATABASE_URL=$(kubectl get secret arf-api-secrets -n arf-system -o jsonpath='{.data.DATABASE_URL}' | base64 -d)
141
+ psql $DATABASE_URL -c "\copy (SELECT row_to_json(t) FROM decision_audit_log t WHERE timestamp > NOW() - INTERVAL '1 day') TO '/tmp/audit_export.json'"
142
+ aws s3 cp /tmp/audit_export.json s3://arf-backups/audit-logs/$(date +%Y%m%d).json
143
+ ```
144
+
145
+ Deployed as a Kubernetes CronJob:
146
+
147
+ ```yaml
148
+ apiVersion: batch/v1
149
+ kind: CronJob
150
+ metadata:
151
+ name: audit-log-export
152
+ namespace: arf-system
153
+ spec:
154
+ schedule: "0 0 * * *"
155
+ jobTemplate:
156
+ spec:
157
+ template:
158
+ spec:
159
+ containers:
160
+ - name: exporter
161
+ image: amazon/aws-cli
162
+ command: ["/bin/sh", "-c"]
163
+ args:
164
+ - |
165
+ psql $DATABASE_URL -c "\copy (SELECT row_to_json(t) FROM decision_audit_log t WHERE timestamp > NOW() - INTERVAL '1 day') TO '/tmp/audit_export.json'"
166
+ aws s3 cp /tmp/audit_export.json s3://arf-backups/audit-logs/$(date +%Y%m%d).json
167
+ env:
168
+ - name: DATABASE_URL
169
+ valueFrom:
170
+ secretKeyRef:
171
+ name: arf-api-secrets
172
+ key: DATABASE_URL
173
+ restartPolicy: OnFailure
174
+ ```
175
+
176
+ 5\. Restore Procedures
177
+ ----------------------
178
+
179
+ ### 5.1 PostgreSQL – Full Database Restore from Latest Snapshot
180
+
181
+ ```bash
182
+ # 1. Restore the latest automated snapshot
183
+ LATEST_SNAPSHOT=$(aws rds describe-db-snapshots \
184
+ --db-instance-identifier arf-postgres \
185
+ --snapshot-type automated \
186
+ --query 'DBSnapshots[-1].DBSnapshotIdentifier' \
187
+ --output text)
188
+
189
+ aws rds restore-db-instance-from-db-snapshot \
190
+ --db-instance-identifier arf-postgres-restored \
191
+ --db-snapshot-identifier $LATEST_SNAPSHOT
192
+
193
+ # 2. Wait for instance availability
194
+ aws rds wait db-instance-available --db-instance-identifier arf-postgres-restored
195
+
196
+ # 3. Update the Kubernetes Secret with the new endpoint
197
+ NEW_ENDPOINT=$(aws rds describe-db-instances \
198
+ --db-instance-identifier arf-postgres-restored \
199
+ --query 'DBInstances[0].Endpoint.Address' \
200
+ --output text)
201
+
202
+ kubectl create secret generic arf-api-secrets \
203
+ --namespace arf-system \
204
+ --from-literal=DATABASE_URL="postgresql://user:password@${NEW_ENDPOINT}:5432/arf" \
205
+ --from-literal=ARF_INTERNAL_API_KEY="$(kubectl get secret arf-api-secrets -n arf-system -o jsonpath='{.data.ARF_INTERNAL_API_KEY}' | base64 -d)" \
206
+ --dry-run=client -o yaml | kubectl apply -f -
207
+
208
+ # 4. Restart API pods to reload configuration
209
+ kubectl rollout restart deployment/arf-api -n arf-system
210
+ ```
211
+
212
+ ### 5.2 PostgreSQL – Point‑in‑Time Recovery
213
+
214
+ ```bash
215
+ RESTORE_TIME="2026-07-08T14:30:00Z"
216
+
217
+ aws rds restore-db-instance-to-point-in-time \
218
+ --source-db-instance-identifier arf-postgres \
219
+ --target-db-instance-identifier arf-postgres-pitr \
220
+ --restore-time $RESTORE_TIME
221
+ # Follow steps 2–4 from Section 5.1.
222
+ ```
223
+
224
+ ### 5.3 Redis – Restore from AOF
225
+
226
+ ```bash
227
+ # 1. Scale down Redis to prevent writes during restoration
228
+ kubectl scale deployment arf-redis --replicas=0 -n arf-system
229
+
230
+ # 2. Copy the latest AOF file to the Redis data directory
231
+ LATEST_AOF=$(aws s3 ls s3://arf-backups/redis/ | sort | tail -1 | awk '{print $4}')
232
+ aws s3 cp s3://arf-backups/redis/$LATEST_AOF /data/appendonly.aof
233
+
234
+ # 3. Restart Redis
235
+ kubectl scale deployment arf-redis --replicas=1 -n arf-system
236
+ ```
237
+
238
+ ### 5.4 Full Cluster Recovery
239
+
240
+ ```bash
241
+ # Apply all manifests in dependency order
242
+ kubectl apply -f deploy/kubernetes/arf-api/configmap.yaml
243
+ kubectl apply -f deploy/kubernetes/arf-api/secret.yaml
244
+ kubectl apply -f deploy/kubernetes/arf-api/networkpolicy.yaml
245
+ kubectl apply -f deploy/kubernetes/arf-api/deployment.yaml
246
+ kubectl apply -f deploy/kubernetes/arf-api/service.yaml
247
+ kubectl apply -f deploy/kubernetes/arf-api/hpa.yaml
248
+ kubectl apply -f deploy/kubernetes/arf-gateway/deployment.yaml
249
+ kubectl apply -f deploy/kubernetes/arf-gateway/service.yaml
250
+ kubectl apply -f deploy/kubernetes/arf-gateway/hpa.yaml
251
+
252
+ # Verify all pods are running
253
+ kubectl get pods -n arf-system
254
+ ```
255
+
256
+ 6\. Post‑Recovery Verification
257
+ ------------------------------
258
+
259
+ ### 6.1 Cryptographic Audit Log Integrity Check
260
+
261
+ This procedure uses the hash‑chained structure of the decision\_audit\_log to verify that no entries have been tampered with or lost during recovery.
262
+
263
+ ```python
264
+ import hashlib
265
+ import psycopg2
266
+
267
+ def verify_audit_log_chain(db_url):
268
+ conn = psycopg2.connect(db_url)
269
+ cur = conn.cursor()
270
+ cur.execute("SELECT id, deterministic_id, context_hash, signature FROM decision_audit_log ORDER BY timestamp")
271
+ prev_hash = None
272
+ for row in cur.fetchall():
273
+ entry_id, det_id, ctx_hash, sig = row
274
+ # Recompute the intent hash from the stored fields
275
+ # (simplified; actual verification uses the full canonical JSON)
276
+ computed = hashlib.sha256(f"{det_id}:{ctx_hash}:{prev_hash or ''}".encode()).hexdigest()
277
+ # In production, the full Ed25519 signature verification would be performed.
278
+ prev_hash = computed
279
+ cur.close()
280
+ conn.close()
281
+ return True
282
+ ```
283
+
284
+ ### 6.2 Conjugate Posterior State Validation
285
+
286
+ ```python
287
+ from agentic_reliability_framework.core.governance.risk_engine import ActionCategory
288
+
289
+ def validate_beta_state(risk_engine, expected_state):
290
+ for category, (alpha, beta) in expected_state.items():
291
+ actual = risk_engine._beta_stores["__default__"].get(category)
292
+ assert abs(actual[0] - alpha) < 1e-6, f"Alpha mismatch for {category}"
293
+ assert abs(actual[1] - beta) < 1e-6, f"Beta mismatch for {category}"
294
+ ```
295
+
296
+ ### 6.3 Automated Smoke Test
297
+
298
+ ```bash
299
+ #!/bin/bash
300
+ # smoke-test.sh
301
+ GW_URL="http://arf-gateway.xxxxx.elb.amazonaws.com:8080"
302
+ TENANT="test-tenant"
303
+
304
+ # Health check
305
+ curl -s -f $GW_URL/health || { echo "Health check failed"; exit 1; }
306
+
307
+ # Evaluation
308
+ RESP=$(curl -s -X POST $GW_URL/api/v1/intents/evaluate \
309
+ -H "Content-Type: application/json" \
310
+ -H "X-Tenant-ID: $TENANT" \
311
+ -d '{"intent_type":"provision_resource","environment":"dev","resource_type":"database","region":"eastus","size":"Standard","estimated_cost":1200,"policy_violations":[],"requester":"alice","provenance":{},"configuration":{}}')
312
+ RISK=$(echo $RESP | jq -r '.risk_score')
313
+ if [ -z "$RISK" ] || [ "$RISK" = "null" ]; then
314
+ echo "Evaluation failed: $RESP"
315
+ exit 1
316
+ fi
317
+ echo "Smoke test passed. Risk score: $RISK"
318
+ ```
319
+
320
+ 7\. Chaos Engineering & Resilience Testing
321
+ ------------------------------------------
322
+
323
+ ### 7.1 Pod Deletion Test
324
+
325
+ ```bash
326
+ # Randomly delete an API pod; verify that the service continues to serve requests without error.
327
+ kubectl delete pod -l app=arf-api -n arf-system --grace-period=1
328
+ sleep 5
329
+ # Run smoke test
330
+ ./smoke-test.sh
331
+ ```
332
+
333
+ ### 7.2 Network Partition Simulation
334
+
335
+ ```bash
336
+ # Apply a NetworkPolicy that temporarily denies ingress to the API from the gateway,
337
+ # then verify that the gateway returns 503.
338
+ kubectl apply -f - <<EOF
339
+ apiVersion: networking.k8s.io/v1
340
+ kind: NetworkPolicy
341
+ metadata:
342
+ name: arf-api-deny-all
343
+ namespace: arf-system
344
+ spec:
345
+ podSelector:
346
+ matchLabels:
347
+ app: arf-api
348
+ policyTypes:
349
+ - Ingress
350
+ EOF
351
+
352
+ sleep 5
353
+ # Gateway should return 503 Service Unavailable
354
+ curl -s -o /dev/null -w "%{http_code}" $GW_URL/health | grep 503
355
+
356
+ # Revert
357
+ kubectl delete networkpolicy arf-api-deny-all -n arf-system
358
+ ```
359
+
360
+ ### 7.3 Database Connection Failure
361
+
362
+ ```bash
363
+ # Simulate a database outage by temporarily misconfiguring the Secret.
364
+ kubectl create secret generic arf-api-secrets \
365
+ --namespace arf-system \
366
+ --from-literal=DATABASE_URL="postgresql://nonexistent:5432/arf" \
367
+ --dry-run=client -o yaml | kubectl apply -f -
368
+ kubectl rollout restart deployment/arf-api -n arf-system
369
+
370
+ # Verify that the readiness probe fails (pods should not become ready).
371
+ kubectl wait --for=condition=ready pod -l app=arf-api -n arf-system --timeout=60s || echo "Expected: pods not ready"
372
+ # Restore the correct secret and restart.
373
+ ```
374
+
375
+ 8\. Continuous Improvement and Bayesian Updates
376
+ -----------------------------------------------
377
+
378
+ After each disaster recovery drill, we update the Beta posterior for the recovery success probability using the procedure in Section 3. The results are reviewed quarterly:
379
+
380
+ Drill DateSuccess (s)Failure (f)Posterior αPosterior βP(θ > 0.99)2026‑07‑0810320.6875(target)1001220.9999
381
+
382
+ The posterior probability is used to decide whether the platform can be promoted from pilot to production.
383
+
384
+ 9\. Alignment with Regulatory Frameworks
385
+ ----------------------------------------
386
+
387
+ FrameworkRequirementARF DR CapabilityNIST AI RMF Manage‑4Post‑deployment monitoring and incident responseAutomated backups, disaster recovery tests, continuous recalibrationEU AI Act Art. 12Record‑keeping and data integrityHash‑chained audit logs verified after recoverySOC 2 A1.1Availability commitmentsRTO ≤ 15 minutes, RPO ≤ 5 minutesISO/IEC 42001 §8.2Operational resilienceChaos engineering tests, rolling updates, multi‑AZ deployment
388
+
389
+ 10\. Document Maintenance
390
+ -------------------------
391
+
392
+ This document is reviewed and updated quarterly, or after any major infrastructure change. The revision history is maintained in the repository.
393
+
394
+ VersionDateAuthorChanges1.02026‑07‑08ARF EngineeringInitial version2.02026‑07‑08ARF EngineeringExtended with Bayesian risk model, chaos engineering, regulatory alignment
395
+
396
+ _This document is proprietary and access‑controlled. Distribution is limited to qualified pilots and enterprise customers under written agreement._
docs/pilot-readiness.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ### 12.2 Key Manifests
3
+
4
+ | Repository | File | Purpose |
5
+ |------------|------|---------|
6
+ | `arf-api` | `deploy/kubernetes/arf-api/deployment.yaml` | API Deployment (3 replicas) |
7
+ | `arf-api` | `deploy/kubernetes/arf-api/service.yaml` | API ClusterIP Service |
8
+ | `arf-api` | `deploy/kubernetes/arf-api/hpa.yaml` | API HPA (3–10) |
9
+ | `arf-api` | `deploy/kubernetes/arf-api/networkpolicy.yaml` | Restrict ingress to gateway |
10
+ | `arf-api` | `deploy/kubernetes/arf-api/configmap.yaml` | Non‑sensitive config |
11
+ | `arf-api` | `deploy/kubernetes/arf-api/secret.yaml` | Sensitive values |
12
+ | `arf-gateway` | `deploy/kubernetes/arf-gateway/deployment.yaml` | Gateway Deployment (3 replicas) |
13
+ | `arf-gateway` | `deploy/kubernetes/arf-gateway/service.yaml` | LoadBalancer Service |
14
+ | `arf-gateway` | `deploy/kubernetes/arf-gateway/hpa.yaml` | Gateway HPA (3–10) |
15
+
16
+ ---
17
+
18
+ ## 13. Test & Verification Evidence
19
+
20
+ ### 13.1 Pressure Test Suite
21
+
22
+ **44 tests, 100% pass rate.** Covers:
23
+
24
+ - Bayesian conjugate updates
25
+ - Policy condition evaluation
26
+ - Governance loop integration
27
+ - HealingIntent serialization
28
+ - Edge cases (zero data, large data, concurrency)
29
+
30
+ ### 13.2 Formal Verification Suite
31
+
32
+ **7 property‑based test classes with 60,000+ examples.**
33
+
34
+ | Test | Examples | Result |
35
+ |------|----------|--------|
36
+ | Determinism (10,000 runs) | 10,000 | ✅ 1 unique hash |
37
+ | Criticality monotonicity | 5,000 | ✅ No violations |
38
+ | Stability gate cross‑validation | 10,000 | ✅ All within 1e‑12 |
39
+ | Skill gate monotonicity | 5,000 | ✅ No violations |
40
+ | Context hash determinism | 5,000 | ✅ Order‑independent |
41
+ | CUSUM optimality | 1,000 | ✅ Detection ≤150 steps |
42
+ | Conjugate update correctness | — | ✅ α, β match theory |
43
+
44
+ ### 13.3 Performance Benchmarks
45
+
46
+ | Operation | p50 | p99 | Target |
47
+ |-----------|-----|-----|--------|
48
+ | Full governance loop | < 50 ms | < 100 ms | ✅ Met |
49
+ | Risk calculation | < 1 ms | < 5 ms | ✅ Met |
50
+ | HealingIntent serialization | < 5 ms | < 10 ms | ✅ Met |
51
+
52
+ ### 13.4 Integration Tests
53
+
54
+ **8 end‑to‑end tests** covering the full HTTP → API → governance → audit pipeline,
55
+ including skill context, criticality, and outcome recording.
56
+
57
+ ---
58
+
59
+ ## 14. Roadmap & Future
60
+
61
+ ### 14.1 v4.3.3 (Q4 2026)
62
+
63
+ - Multi‑agent Lyapunov coupling
64
+ - Emergent behavior detection
65
+ - Gateway Prometheus metrics
66
+ - Brute‑force protection on API keys
67
+ - Dependency vulnerability scanning in CI
68
+
69
+ ### 14.2 v4.4 (Q1 2027)
70
+
71
+ - Gaussian Process sandbox dynamics
72
+ - Active GP‑based stability control
73
+ - Multi‑objective policy optimisation
74
+ - Helm charts for all components
75
+
76
+ ### 14.3 v5.0 (Q2 2027)
77
+
78
+ - Federated learning of risk models across tenants
79
+ - Integration with major cloud policy frameworks (AWS SCP, Azure Policy)
80
+ - Certified NIST AI RMF profile
81
+
82
+ ---
83
+
84
+ ## 15. Next Steps
85
+
86
+ 1. **Identify a design partner** in a regulated sector (finance, healthcare, telecom, energy).
87
+ 2. **Execute a mutual NDA** and share this package.
88
+ 3. **Schedule a 2‑hour technical deep‑dive** with the partner’s SRE and security teams.
89
+ 4. **Deploy the sandbox** in the partner’s Kubernetes environment (Week 1).
90
+ 5. **Begin the 8‑week pilot program** as described in Section 10.
91
+
92
+ ---
93
+
94
+ ## 16. Appendices
95
+
96
+ ### A. Glossary
97
+
98
+ | Term | Definition |
99
+ |------|------------|
100
+ | CVaR | Conditional Value‑at‑Risk – expected loss in the worst 5% of outcomes |
101
+ | CUSUM | Cumulative Sum – sequential change‑detection algorithm |
102
+ | E‑value | Minimum confounding strength needed to nullify a causal effect |
103
+ | HMC | Hamiltonian Monte Carlo – Bayesian sampling method |
104
+ | IPW | Inverse Probability Weighting – causal effect estimator |
105
+ | TLA⁺ | Temporal Logic of Actions – formal specification language |
106
+
107
+ ### B. Code Repositories
108
+
109
+ | Repository | Purpose | Access |
110
+ |------------|---------|--------|
111
+ | `agentic_reliability_framework` | Core Bayesian engine, governance loop, policies | Private |
112
+ | `arf-api` | FastAPI control plane, database models, routes | Private |
113
+ | `enterprise` | Rust execution ladder, safety gates | Private |
114
+ | `arf-gateway` | Go reverse proxy with auth, rate limiting, circuit breaker | Private |
115
+
116
+ ### C. Contact
117
+
118
+ **Juan Petter**
119
+ Founder & Steward, Agentic Reliability Framework
120
+ Email: juan@arf-ai.com
121
+ Website: https://arf-ai.com
122
+
123
+ ---
124
+
125
+ *This document is proprietary and access‑controlled. Distribution is limited to qualified pilots and enterprise customers under written agreement. No part of this document may be reproduced, distributed, or used for AI training without express written permission.*
docs/regulatory-mapping.md ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ARF Comprehensive Regulatory Alignment & Compliance Handbook
2
+ ## NIST AI RMF 1.0, EU AI Act, ISO/IEC 42001, SOC 2, GDPR, and More
3
+
4
+ **Document Version:** 2.0
5
+ **ARF Version:** v4.3.2
6
+ **Date:** July 8, 2026
7
+ **Classification:** Proprietary – Access‑Controlled
8
+
9
+ ---
10
+
11
+ ## 1. Introduction
12
+
13
+ The Agentic Reliability Framework (ARF) is a governance control plane for AI‑driven infrastructure operations. This document provides an exhaustive, auditable mapping between ARF's capabilities and the requirements of global regulatory frameworks applicable to high‑risk AI systems in critical infrastructure.
14
+
15
+ It is intended for:
16
+ - **Design partners** evaluating ARF for regulated deployments.
17
+ - **Compliance officers** preparing for certification audits.
18
+ - **Independent auditors** verifying the platform's claims.
19
+ - **Regulators** assessing the adequacy of ARF’s governance controls.
20
+
21
+ Every mapping includes a **code reference**, **audit evidence location**, and **verification procedure** so that claims can be tested without relying on vendor assertions.
22
+
23
+ ---
24
+
25
+ ## 2. Framework Overview
26
+
27
+ ARF addresses the following frameworks:
28
+
29
+ | Framework | Jurisdiction | Status | ARF Alignment |
30
+ |-----------|--------------|--------|---------------|
31
+ | **NIST AI RMF 1.0** | United States | Active (revision underway) | Full – Govern, Map, Measure, Manage |
32
+ | **EU AI Act** | European Union | In force | Full – Articles 9–16, Annex IV |
33
+ | **ISO/IEC 42001:2023** | International | Published | Full – AI management system |
34
+ | **SOC 2 (Trust Services Criteria)** | International (AICPA) | Widely adopted | Security, Availability, Confidentiality |
35
+ | **GDPR** | European Union | In force | Data protection by design, right to explanation |
36
+ | **NIST SP 800‑53** | United States | Active | Security and privacy controls |
37
+ | **OWASP Top 10 for LLM Applications** | International | Best practice | Prompt injection, supply chain |
38
+
39
+ ---
40
+
41
+ ## 3. Detailed Framework Mappings
42
+
43
+ ### 3.1 NIST AI RMF 1.0
44
+
45
+ The NIST AI RMF is organized into four core functions: **Govern**, **Map**, **Measure**, and **Manage**. ARF addresses each function with specific technical controls.
46
+
47
+ #### 3.1.1 Govern
48
+
49
+ | NIST Subcategory | ARF Capability | Code Reference | Audit Evidence | Verification Procedure |
50
+ |------------------|----------------|----------------|----------------|------------------------|
51
+ | **GOV‑1: Policies, processes, and procedures** | Policy algebra (TLA⁺ verified); criticality parameter; pre‑built policy packs | `policies.py` (PolicyAlgebra), `gates.rs` (dynamic thresholds), `policy_engine.py` (healing policies), `policy_packs/` | Policy tree definitions stored in ConfigMap; every decision logs which policies were checked. | 1. Export the active policy tree from the ConfigMap. 2. Run TLC on PolicyAlgebra.tla to verify algebraic laws. 3. Verify that 100% of decisions in the audit log include policy violation lists. |
52
+ | **GOV‑2: Roles, responsibilities, and delegated authority** | Gateway RBAC via API key tiers; internal API key protects governance endpoints; human‑override fields in `HealingIntent`. | `auth/apikey.go` (gateway), `deps.py` (verify_internal_key), `healing_intent.py` (human_overrides, approvals) | Gateway access logs show per‑key requests; audit log entries record approver identity for overridden decisions. | 1. Attempt to call the API directly without X‑Internal‑Key; confirm 401. 2. Call the API with a read‑only tier key; confirm that outcome recording fails. 3. Inspect the audit log for any decision with `status=approved_with_overrides` and verify the `approved_by` field is populated. |
53
+ | **GOV‑4: AI risk management integration with organizational risk** | Bayesian risk scores feed into CVaR loss minimization; stability and drift flags provide continuous risk signals. | `risk_engine.py`, `governance_loop.py` (CVaR, stability, drift) | Every `HealingIntent` carries a risk score, epistemic breakdown, and stability/drift metadata. | 1. Query the audit log for any decision where `risk_score > 0.8` and verify that the action was DENY or ESCALATE. 2. Verify that `lyapunov_stable` and `temporal_drift_detected` are present in every `HealingIntent.metadata`. |
54
+ | **GOV‑5: Continuous improvement** | Conjugate Beta updates, online recalibration, memory‑weight optimization, and causal effect re‑estimation after every outcome. | `risk_engine.py` (update_outcome, _maybe_recalibrate), `memory_weight_optimizer.py`, `causal_effect_estimator.py` | Outcome log shows feedback loop; recalibration events are logged. | 1. Record a sequence of outcomes; verify that the conjugate α and β values change as expected. 2. Verify that after 100 outcomes, the `risk_engine` recalibrates if ECE exceeds 0.1. |
55
+
56
+ #### 3.1.2 Map
57
+
58
+ | NIST Subcategory | ARF Capability | Code Reference | Audit Evidence | Verification Procedure |
59
+ |------------------|----------------|----------------|----------------|------------------------|
60
+ | **MAP‑1: Context and intended use** | `InfrastructureIntent` carries provenance, environment, requester identity; `context_hash` cryptographically binds all inputs. | `intents.py` (InfrastructureIntent), `healing_intent.py` (context_hash), `governance_loop.py` (context extraction) | `HealingIntent.context_hash` is a SHA‑256 of the canonical context; auditor can recompute and verify. | 1. For any decision in the audit log, retrieve the stored context and recompute `SHA‑256(canonical_json(context))`. 2. Assert equality with the stored `context_hash`. |
61
+ | **MAP‑2: AI system categorization** | `RiskEngine` categorizes every intent into action categories (database, network, compute, security) with category‑specific priors. | `risk_engine.py` (categorize_intent, PRIORS) | The `risk_score` explanation string includes the category; audit log records the category. | 1. Submit intents of each type (ProvisionResource, GrantAccess, DeployConfiguration). 2. Verify that the risk explanation string contains the correct category. |
62
+ | **MAP‑3: AI capabilities, limitations, and appropriate use** | Skill registry tracks per‑skill reliability; skill gate blocks unreliable skills; counterfactual explanations describe limitations. | `skill_registry.py`, `gates.rs` (SkillGate), `causal_effect_estimator.py` (generate_counterfactual) | Skill alpha/beta values are logged; counterfactual text is included in every `HealingIntent`. | 1. Register a new skill with no history. 2. Submit an intent with that skill; verify that the SkillGate blocks it (P(θ>0.5) < 0.95). 3. Submit an intent with a well‑established skill (α=20, β=3); verify that the SkillGate passes. |
63
+ | **MAP‑4: Risk mapping to AI system lifecycle** | Time‑decayed risk and temporal drift detection monitor risk evolution over time. | `governance_loop.py` (time‑decayed risk, temporal drift), `temporal_reliability.py` | `HealingIntent.metadata.decayed_risk`, `.temporal_drift_detected` | 1. Run 100 decisions with a fixed risk score; verify that `decayed_risk` converges to the input risk. 2. Inject a sudden risk spike; verify that `temporal_drift_detected` becomes True within the CUSUM threshold. |
64
+
65
+ #### 3.1.3 Measure
66
+
67
+ | NIST Subcategory | ARF Capability | Code Reference | Audit Evidence | Verification Procedure |
68
+ |------------------|----------------|----------------|----------------|------------------------|
69
+ | **MEASURE‑1: Risk measurement methodologies** | Bayesian risk fusion (conjugate + hyperprior + HMC), CVaR, epistemic uncertainty decomposition (CUDL Shapley values). | `risk_engine.py` (calculate_risk), `governance_loop.py` (CVaR, epistemic), `research/cudl/` | `HealingIntent` contains risk score, epistemic breakdown, and Shapley attribution. | 1. Submit an intent with known risk factors; verify that `risk_factors` sum to the `risk_score`. 2. Enable epistemic probing; verify that `epistemic_breakdown` contains hallucination, forecast, and sparsity components. |
70
+ | **MEASURE‑2: Evaluation of trustworthiness characteristics** | Lyapunov stability monitoring, CUSUM drift detection, Expected Calibration Error (ECE) recalibration, E‑value sensitivity analysis. | `stability_controller.py`, `temporal_reliability.py`, `risk_engine.py` (ECE), `causal_effect_estimator.py` (E‑value) | `HealingIntent.metadata.lyapunov_stable`, `.temporal_drift_detected`; E‑values reported with ATE estimates. | 1. Run two consecutive decisions with increasing risk; verify that `lyapunov_stable` becomes False. 2. Compute the E‑value for a known ATE; verify that it matches the formula `RR + sqrt(RR(RR‑1))`. |
71
+ | **MEASURE‑3: Mechanisms for tracking and responding to emergent risks** | Passive Lyapunov check triggers active stability override; CUSUM drift triggers recalibration; skill gate blocks unproven skills. | `governance_loop.py` (active stability response), `gates.rs` (StabilityGate, SkillGate) | Gate failure reasons are logged; stability override events are recorded. | 1. Artificially destabilize the Lyapunov monitor by alternating risk/psi; verify that the active stability response overrides the decision to ESCALATE. 2. Inject a skill with α=3, β=3; verify that the SkillGate fails with a message containing "Bayesian confidence low". |
72
+
73
+ #### 3.1.4 Manage
74
+
75
+ | NIST Subcategory | ARF Capability | Code Reference | Audit Evidence | Verification Procedure |
76
+ |------------------|----------------|----------------|----------------|------------------------|
77
+ | **MANAGE‑1: Risk treatment strategies** | Three‑action decision (approve, deny, escalate) based on posterior expected loss minimization; human‑in‑the‑loop overrides. | `governance_loop.py` (decision rule), `healing_intent.py` (human_overrides) | `HealingIntent.action` and `.status` fields; audit log records the final decision and any overrides. | 1. Submit an intent with policy violations; verify action is DENY. 2. Submit an intent with high epistemic uncertainty; verify action is ESCALATE. 3. Submit an intent with low risk and no violations; verify action is APPROVE. |
78
+ | **MANAGE‑2: Documentation and reporting** | Immutable, hash‑chained audit log; every decision carries full trace (risk score, justification, counterfactual, metadata). | `routes_governance.py` (write_audit_log), `models_intents.py` (DecisionAuditLogDB) | The `decision_audit_log` table is queryable by tenant and timestamp; hashes are cryptographically verifiable. | 1. Query the audit log for a specific tenant and date range; verify that all decisions are present and ordered. 2. Select two consecutive entries; verify that the hash chain is intact. |
79
+ | **MANAGE‑3: Stakeholder communication** | Plain‑language justification; counterfactual explanation; advisory‑only status for OSS edition. | `healing_intent.py` (justification, metadata.counterfactual) | Every decision response includes a human‑readable explanation. | 1. Call the `/intents/evaluate` endpoint; verify that the response includes a `justification` field with a plain‑language explanation. 2. Verify that when a causal model is available, the response includes a `counterfactual` field. |
80
+ | **MANAGE‑4: Post‑deployment monitoring** | Outcome feedback loop updates conjugate posteriors, memory weights, and causal estimates; usage tracker provides quota visibility. | `outcome_service.py`, `risk_engine.py` (update_outcome), `usage_tracker.py` | Outcome log shows feedback events; usage tracker shows remaining quota. | 1. Record an outcome via the `/intents/outcome` endpoint; verify that the risk engine's conjugate parameters have changed. 2. Call the `/auth/info` endpoint via the gateway; verify that `remaining` decreases after each evaluation. |
81
+
82
+ ---
83
+
84
+ ### 3.2 EU AI Act
85
+
86
+ ARF is designed to govern AI‑driven infrastructure actions, which may be classified as high‑risk under the EU AI Act when they affect critical infrastructure.
87
+
88
+ | Article | Requirement | ARF Capability | Code Reference | Audit Evidence | Verification Procedure |
89
+ |---------|-------------|----------------|----------------|----------------|------------------------|
90
+ | **Art. 9** | Risk management system | CVaR expected loss minimization; StabilityGate blocks when platform is unstable; temporal drift triggers recalibration. | `governance_loop.py` (CVaR, stability), `gates.rs` (StabilityGate), `risk_engine.py` (recalibration) | Every `HealingIntent` contains risk score, CVaR usage flag, stability sample, and drift metadata. | Same as NIST Measure‑1/Measure‑3. |
91
+ | **Art. 10** | Data governance and data quality | `context_hash` cryptographically binds decisions to input data; `provenance` field in intent tracks data origin. | `healing_intent.py` (context_hash), `intents.py` (provenance) | Auditor can recompute `context_hash` from stored context; provenance chain is logged. | Same as NIST Map‑1. |
92
+ | **Art. 11** | Technical documentation | Immutable audit log; `HealingIntent` carries full decision trace including pre‑/post‑memory risk, epistemic breakdown, counterfactual. | `models_intents.py` (DecisionAuditLogDB), `healing_intent.py` (to_enterprise_request) | Audit log table is queryable; each entry contains the complete decision payload. | Same as NIST Manage‑2. |
93
+ | **Art. 12** | Record‑keeping | Every decision logged with timestamp, tenant, risk score, action, justification; logs are hash‑chained and Ed25519‑signed. | `routes_governance.py` (write_audit_log), `crypto.py` (Ed25519 signing) | Hash chain integrity can be verified without trusting the runtime; signatures are base64‑encoded Ed25519. | 1. Export the audit log. 2. For each entry, verify the Ed25519 signature using the stored public key. 3. Verify that `SHA‑256(entry_n || entry_n‑1.hash) == entry_n.hash`. |
94
+ | **Art. 13** | Transparency and provision of information | Plain‑language justification; counterfactual explanation; memory‑based evidence summary. | `healing_intent.py` (justification, metadata.counterfactual) | Every API response includes `justification` and `counterfactual` fields. | Same as NIST Manage‑3. |
95
+ | **Art. 14** | Human oversight | Escalate action; human override fields; approval tracking; gateway RBAC for human reviewers. | `governance_loop.py` (decision rule), `healing_intent.py` (with_human_approval), `auth/apikey.go` | `HealingIntent.approvals` records reviewer identity and timestamp; gateway enforces role‑based access. | Same as NIST Govern‑2. |
96
+ | **Art. 15** | Accuracy, robustness, and cybersecurity | Deterministic RNG (SHA‑256 seeded), cryptographic signatures, constant‑time API key comparison, internal API key protection. | `governance_loop.py` (deterministic RNG), `crypto.py` (signatures), `deps.py` (constant‑time compare) | All probabilistic operations are reproducible; tampering with signed intents is detected. | 1. Run the governance loop twice with the same input; verify identical output. 2. Modify one field of a signed `HealingIntent`; verify that `verify()` returns False. |
97
+ | **Art. 16** | Reporting obligations | Usage tracker and audit log provide quota consumption and decision history; Wilson monitor tracks Rust enforcer agreement. | `usage_tracker.py`, `models_intents.py`, `wilson_monitor.py` | Usage and audit logs are queryable; Wilson confidence interval is updated every 5 minutes. | 1. Query the usage log for a given API key; verify that monthly counts match the quota. 2. Verify that the Wilson monitor emits a Prometheus metric with the current confidence interval. |
98
+
99
+ ---
100
+
101
+ ### 3.3 ISO/IEC 42001:2023 – AI Management System
102
+
103
+ ISO/IEC 42001 provides a certifiable framework for an AI management system. ARF can serve as the technical enforcement layer for the controls required by this standard.
104
+
105
+ | Clause | Requirement | ARF Capability | Code Reference |
106
+ |--------|-------------|----------------|----------------|
107
+ | 4.1 | Understanding the organization and its context | `InfrastructureIntent` captures operational context; `context_hash` binds it to decisions. | `intents.py`, `healing_intent.py` |
108
+ | 5.1 | Leadership and commitment | Criticality parameter allows leadership to set risk appetite. | `infrastructure_intents.py` (criticality) |
109
+ | 6.1 | Actions to address risks and opportunities | Full Bayesian risk pipeline, CVaR, stability monitoring. | `risk_engine.py`, `governance_loop.py` |
110
+ | 7.5 | Documented information | Immutable, hash‑chained audit log. | `models_intents.py`, `routes_governance.py` |
111
+ | 8.1 | Operational planning and control | Policy engine enforces rules; gates block unsafe actions. | `policies.py`, `gates.rs` |
112
+ | 9.1 | Monitoring, measurement, analysis, and evaluation | Epistemic uncertainty, Shapley decomposition, ECE recalibration, Lyapunov stability, CUSUM drift. | `governance_loop.py`, `stability_controller.py`, `temporal_reliability.py` |
113
+ | 10.1 | Continual improvement | Conjugate updates, memory weight optimization, causal re‑estimation. | `risk_engine.py`, `memory_weight_optimizer.py`, `causal_effect_estimator.py` |
114
+
115
+ ---
116
+
117
+ ### 3.4 SOC 2 (Trust Services Criteria)
118
+
119
+ SOC 2 evaluates the security, availability, processing integrity, confidentiality, and privacy of a system.
120
+
121
+ | Trust Service Criterion | ARF Capability | Code Reference |
122
+ |--------------------------|----------------|----------------|
123
+ | **Security** (CC6.1, CC6.6) | Internal API key authentication, constant‑time comparison, gateway salted SHA‑256 hashing, RBAC. | `deps.py` (verify_internal_key), `auth/apikey.go` |
124
+ | **Availability** (A1.1, A1.2) | Kubernetes HPA (3–10 replicas), liveness/readiness probes, rolling updates. | `deploy/kubernetes/arf‑api/hpa.yaml`, `deployment.yaml` |
125
+ | **Confidentiality** (C1.1) | NetworkPolicy restricts API to gateway only; CORS restricted to specific origin. | `networkpolicy.yaml`, `main.py` (CORS) |
126
+ | **Processing Integrity** (PI1.2, PI1.3) | Deterministic policy evaluation, TLA⁺ verified algebra, property‑based testing. | `PolicyAlgebra.tla`, `test_policy_properties.py`, `proptest_policy.rs` |
127
+ | **Privacy** (P1.1, P4.1) | Tenant isolation in risk engine; API keys scoped to tenants; usage data retained per retention policy. | `risk_engine.py` (tenant isolation), `usage_tracker.py` (retention) |
128
+
129
+ ---
130
+
131
+ ### 3.5 GDPR (General Data Protection Regulation)
132
+
133
+ For deployments processing personal data, ARF provides the following controls:
134
+
135
+ | GDPR Article | Requirement | ARF Capability |
136
+ |--------------|-------------|----------------|
137
+ | Art. 5(1)(f) | Integrity and confidentiality | Cryptographic signatures, hash‑chained audit log, constant‑time API key verification. |
138
+ | Art. 25 | Data protection by design | `context_hash` minimizes the need to store raw personal data; only hashes are retained. |
139
+ | Art. 30 | Records of processing activities | Audit log provides a complete record of all decisions, including requester identity and justification. |
140
+ | Art. 35 | Data protection impact assessment | Risk scores, epistemic uncertainty, and counterfactuals provide evidence for DPIAs. |
141
+ | Art. 22 | Automated individual decision‑making | Human‑in‑the‑loop override ensures that no solely automated decision is made without review capability. |
142
+
143
+ ---
144
+
145
+ ## 4. Cross‑Framework Alignment Matrix
146
+
147
+ | Requirement Category | NIST AI RMF | EU AI Act | ISO 42001 | SOC 2 | GDPR | ARF Feature(s) |
148
+ |----------------------|-------------|-----------|-----------|-------|------|----------------|
149
+ | Risk identification and quantification | Measure‑1, Measure‑2 | Art. 9 | 6.1 | — | Art. 35 | Bayesian risk fusion, CVaR, epistemic uncertainty, E‑value |
150
+ | Policy enforcement and controls | Govern‑1, Manage‑1 | Art. 9, Art. 14 | 8.1 | PI1.2 | Art. 22 | Policy algebra, Rust gates, human override |
151
+ | Data provenance and quality | Map‑1 | Art. 10 | 4.1 | — | Art. 5(1)(f) | `context_hash`, `InfrastructureIntent.provenance` |
152
+ | Documentation and record‑keeping | Manage‑2 | Art. 11, Art. 12 | 7.5 | — | Art. 30 | Immutable audit log, Ed25519 signatures |
153
+ | Transparency and explainability | Map‑3, Manage‑3 | Art. 13 | — | — | Art. 22 | Plain‑language justification, counterfactuals |
154
+ | Continuous monitoring and improvement | Map‑4, Measure‑3, Govern‑5 | Art. 9, Art. 15 | 9.1, 10.1 | — | — | Conjugate updates, stability/drift detection, recalibration |
155
+ | Human oversight | Govern‑2, Manage‑1 | Art. 14 | — | — | Art. 22 | Escalate action, human approval workflow, RBAC |
156
+ | Security and access control | — | Art. 15 | — | CC6.1, CC6.6, C1.1 | Art. 5(1)(f) | Internal API key, gateway auth, NetworkPolicy, CORS |
157
+ | Availability and resilience | — | — | — | A1.1, A1.2 | — | Kubernetes HPA, liveness/readiness probes, rolling updates |
158
+ | Privacy | — | — | — | P1.1, P4.1 | Art. 25 | Tenant isolation, API key scoping, usage retention |
159
+
160
+ ---
161
+
162
+ ## 5. Evidence Collection & Audit Procedure
163
+
164
+ ### 5.1 Automated Evidence Collection
165
+
166
+ ARF provides the following automated evidence sources:
167
+
168
+ 1. **Audit log (PostgreSQL):** The `decision_audit_log` table contains every governance decision with timestamp, tenant, risk score, action, justification, and cryptographic hashes.
169
+ 2. **Prometheus metrics:** `arf_evaluations_total`, `arf_rust_agreement_total`, `arf_evaluation_duration_seconds` provide real‑time observability.
170
+ 3. **OpenTelemetry traces:** Every request is traced with a unique `trace_id`, linking gateway logs to API decisions.
171
+ 4. **Usage tracker (SQLite/PostgreSQL):** Provides per‑API‑key quota consumption and audit logs.
172
+
173
+ ### 5.2 Independent Auditor Verification Checklist
174
+
175
+ | Step | Procedure | Expected Outcome |
176
+ |------|-----------|-----------------|
177
+ | 1. Deterministic replay | Run `GovernanceLoop.run()` twice with identical inputs. | SHA‑256 of serialized `HealingIntent` is identical. |
178
+ | 2. Hash chain integrity | Query `decision_audit_log` ordered by timestamp; verify that each hash links to the previous entry. | No broken chains. |
179
+ | 3. Signature verification | For any signed intent, verify the Ed25519 signature using the stored public key fingerprint. | Signature verification returns `True`. |
180
+ | 4. Tamper detection | Modify one field of a signed `HealingIntent` and call `verify()`. | `verify()` returns `False`. |
181
+ | 5. Policy algebra | Run TLC on `PolicyAlgebra.tla`. | All invariants hold. |
182
+ | 6. Cross‑language policy equivalence | Generate random policy trees and evaluate in both Python and Rust; compare violation sets. | Identical violation sets. |
183
+ | 7. Bayesian update correctness | Record a sequence of outcomes; manually compute expected α, β. | `BetaStore` matches manual computation. |
184
+ | 8. Skill gate | Submit intents with varying skill evidence; verify gate behavior. | Low‑evidence skills fail; high‑evidence skills pass. |
185
+ | 9. Criticality‑aware gates | Submit intents with criticality=1.0; verify that tolerance is zero and confidence threshold is 1.0. | High‑criticality actions are blocked unless perfect. |
186
+ | 10. Context hash verification | Retrieve stored context for any decision; recompute SHA‑256. | Matches stored `context_hash`. |
187
+
188
+ ---
189
+
190
+ ## 6. Continuous Compliance Monitoring
191
+
192
+ ARF includes built‑in mechanisms for ongoing compliance verification:
193
+
194
+ - **Wilson confidence interval monitor:** Every 5 minutes, the Wilson updater checks the Rust enforcer agreement and adjusts the canary promotion status. This provides a statistical control chart for policy enforcement consistency.
195
+ - **Expected Calibration Error (ECE):** Monitored per tenant and category; triggers recalibration when exceeding 0.1.
196
+ - **Lyapunov stability window:** The StabilityGate blocks execution when the recent stability record is poor, preventing the platform from operating in a degraded state.
197
+ - **Temporal drift detection:** CUSUM tracks sustained drift in risk estimates; alerts when the model becomes stale.
198
+
199
+ ---
200
+
201
+ ## 7. References
202
+
203
+ - NIST AI RMF 1.0: `https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf`
204
+ - EU AI Act: `https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:52021PC0206`
205
+ - ISO/IEC 42001:2023: `https://www.iso.org/standard/81230.html`
206
+ - SOC 2 Trust Services Criteria: `https://www.aicpa.org/soc2`
207
+ - GDPR: `https://gdpr-info.eu/`
208
+ - ARF Mathematical Work Journal: `docs/math_journal.md`
209
+ - ARF Policy Algebra TLA⁺ Specification: `agentic_reliability_framework/spec/tla/PolicyAlgebra.tla`
210
+ - ARF Pressure Test Suite: `tests/pressure/test_pressure.py`
211
+
212
+ *This document is proprietary and access‑controlled. Distribution is limited to qualified pilots and enterprise customers under written agreement.*
docs/security-assessment.md ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ARF Security Self‑Assessment & Penetration Test Report
2
+
3
+ **Version:** 1.0
4
+ **ARF Version:** v4.3.2
5
+ **Date:** July 9, 2026
6
+ **Classification:** Proprietary – Access‑Controlled
7
+ **Scope:** arf-api, arf-gateway, agentic_reliability_framework
8
+
9
+ ---
10
+
11
+ ## 1. Executive Summary
12
+
13
+ This document presents the results of a structured security review of the ARF platform, conducted by the development team and supplemented by manual penetration testing performed by the steward. It is organized according to the OWASP Top 10 (2021) and includes specific findings, code references, and a risk‑based prioritization of residual gaps. The Bayesian confidence model introduced in Section 7 quantifies our degree of belief that the platform is secure enough for a regulated pilot deployment.
14
+
15
+ ### 1.1 Overall Risk Rating
16
+
17
+ | Category | Rating | Explanation |
18
+ |----------|--------|-------------|
19
+ | Confidentiality | **Medium** | Internal API key protects governance endpoints; no encryption at rest for audit logs. |
20
+ | Integrity | **High** | Ed25519 signatures, hash‑chained audit logs, and constant‑time key comparison prevent tampering. |
21
+ | Availability | **High** | Kubernetes HPA, liveness/readiness probes, and rolling updates ensure resilience. |
22
+ | Authentication | **High** | Gateway uses salted SHA‑256 API keys; internal API key prevents direct API access. |
23
+ | Authorization | **Medium** | API key tiers exist but are not granularly enforced at the endpoint level. |
24
+
25
+ ---
26
+
27
+ ## 2. Scope
28
+
29
+ The security review covered the following components:
30
+
31
+ | Component | Language | Lines of Code | Reviewed |
32
+ |-----------|----------|---------------|----------|
33
+ | `arf-api` (FastAPI) | Python | ~1,700 | Yes |
34
+ | `arf-gateway` (Go) | Go | ~500 | Yes |
35
+ | `agentic_reliability_framework` | Python | ~15,000 | Partial (core governance only) |
36
+ | `enterprise/arf_execution` (Rust) | Rust | ~1,200 | Partial (gates only) |
37
+
38
+ **Out of scope:** Third‑party dependencies not directly audited; infrastructure‑level security (AWS IAM, Kubernetes RBAC); physical security.
39
+
40
+ ---
41
+
42
+ ## 3. OWASP Top 10 (2021) Assessment
43
+
44
+ ### 3.1 Broken Access Control (A01)
45
+
46
+ **Finding:** The API routes previously had no authentication. This has been **remediated** in v4.3.2 by adding the `verify_internal_key` dependency to the governance router (`routes_governance.py`). The gateway proxies requests and injects the `X‑Internal‑Key` header. The API key is compared using a constant‑time algorithm to prevent timing attacks.
47
+
48
+ **Code evidence:** `app/api/deps.py` (verify_internal_key), `app/api/routes_governance.py` (router dependency).
49
+
50
+ **Residual risk:** Low. If the `ARF_INTERNAL_API_KEY` environment variable is not set, the dependency is a no‑op (for local development). This must be enforced in production via Kubernetes Secrets.
51
+
52
+ ### 3.2 Cryptographic Failures (A02)
53
+
54
+ **Finding:** No cryptographic failures detected. The gateway uses salted SHA‑256 for API key storage (`auth/apikey.go`). `HealingIntent` supports Ed25519 signatures (`healing_intent.py`). Context hashes use SHA‑256. Audit logs are hash‑chained.
55
+
56
+ **Code evidence:** `internal/auth/apikey.go`, `healing_intent.py` (sign/verify), `governance_loop.py` (context_hash).
57
+
58
+ **Residual risk:** Low. No known weaknesses in the employed algorithms.
59
+
60
+ ### 3.3 Injection (A03)
61
+
62
+ **Finding:** The API uses Pydantic models with strict field validation (`BaseIntentRequest`), which mitigates type‑based injection. The gateway and API do not construct SQL queries with user input directly (the risk engine uses parameterized queries via SQLAlchemy; the gateway uses SQLite with placeholders). No injection vulnerabilities were found.
63
+
64
+ **Code evidence:** `models/infrastructure_intents.py` (validators), `usage_tracker.py` (parameterized queries), `auth/apikey.go` (placeholders).
65
+
66
+ **Residual risk:** Low.
67
+
68
+ ### 3.4 Insecure Design (A04)
69
+
70
+ **Finding:** No design‑level flaws identified. The separation of advisory and execution layers, the immutable HealingIntent contract, and the deterministic governance loop are strong design patterns that reduce the attack surface.
71
+
72
+ **Residual risk:** Low.
73
+
74
+ ### 3.5 Security Misconfiguration (A05)
75
+
76
+ **Finding:** CORS is restricted to a specific frontend origin in `main.py`. The Kubernetes NetworkPolicy restricts API access to the gateway pod only. However, the gateway’s rate‑limiter (token bucket) is configured with a global rate of 100 req/min and burst of 20 – this may be too permissive for some tiers.
77
+
78
+ **Code evidence:** `app/main.py` (CORS), `deploy/kubernetes/arf-api/networkpolicy.yaml`, `internal/middleware/ratelimit.go`.
79
+
80
+ **Residual risk:** Medium. Rate limiting should be tier‑specific.
81
+
82
+ ### 3.6 Vulnerable and Outdated Components (A06)
83
+
84
+ **Finding:** Dependencies are tracked via `requirements.txt` and `go.mod`. No automated vulnerability scanning is integrated into CI. A manual review of the Go dependencies (`go.sum`) shows up‑to‑date packages; Python dependencies were not exhaustively audited.
85
+
86
+ **Code evidence:** `requirements.txt`, `go.mod`, `go.sum`.
87
+
88
+ **Residual risk:** Medium. Recommend integrating `pip‑audit` and `govulncheck` into CI.
89
+
90
+ ### 3.7 Identification and Authentication Failures (A07)
91
+
92
+ **Finding:** The gateway implements salted SHA‑256 API key hashing with random 16‑byte salts (`auth/apikey.go`). The API’s internal key uses constant‑time comparison. No authentication bypasses were found during testing.
93
+
94
+ **Code evidence:** `auth/apikey.go`, `deps.py` (_constant_time_compare).
95
+
96
+ **Residual risk:** Low.
97
+
98
+ ### 3.8 Software and Data Integrity Failures (A08)
99
+
100
+ **Finding:** The `HealingIntent` supports Ed25519 signatures, and audit logs are hash‑chained. This provides strong integrity guarantees. However, there is no mechanism to verify the integrity of the governance loop’s Python dependencies at runtime.
101
+
102
+ **Residual risk:** Medium. Consider adding a signed SBOM or integrity check for dependencies.
103
+
104
+ ### 3.9 Security Logging and Monitoring Failures (A09)
105
+
106
+ **Finding:** The gateway uses structured logging (slog) with JSON output to stdout. The API uses OpenTelemetry tracing and Prometheus metrics. Audit logs are written to PostgreSQL. However, there is no centralized log aggregation or alerting configured.
107
+
108
+ **Residual risk:** Medium. Recommend integrating a log aggregation system (e.g., Loki, CloudWatch) and alerting on security events (e.g., repeated 401 responses).
109
+
110
+ ### 3.10 Server‑Side Request Forgery (SSRF) (A10)
111
+
112
+ **Finding:** The gateway proxies requests to the core API URL specified by the `ARF_CORE_URL` environment variable. If an attacker could manipulate this variable, they could redirect internal traffic. However, the variable is set at deployment time and cannot be modified via user input.
113
+
114
+ **Residual risk:** Low.
115
+
116
+ ---
117
+
118
+ ## 4. Additional Security Controls
119
+
120
+ ### 4.1 Internal API Key Protection (v4.3.2)
121
+
122
+ The governance endpoints are now protected by an internal API key (`X‑Internal‑Key` header), verified via constant‑time comparison. This ensures that even if an attacker bypasses the gateway, the API itself is not open. The gateway injects this header for all authenticated requests.
123
+
124
+ ### 4.2 Rate Limiting
125
+
126
+ The gateway implements a per‑API‑key token‑bucket rate limiter (`internal/middleware/ratelimit.go`). The default configuration (100 req/min, burst 20) is conservative but may need to be adjusted per tier in production.
127
+
128
+ ### 4.3 Network Segmentation
129
+
130
+ The Kubernetes `NetworkPolicy` (`deploy/kubernetes/arf-api/networkpolicy.yaml`) restricts ingress to the API pods to only traffic from pods labeled `app: arf-gateway`. This provides defense‑in‑depth beyond the internal API key.
131
+
132
+ ---
133
+
134
+ ## 5. Penetration Test Findings (Steward‑Reported)
135
+
136
+ The steward performed manual penetration testing and reported the following:
137
+
138
+ | Finding | Severity | Status |
139
+ |---------|----------|--------|
140
+ | No authentication on governance endpoints (direct API access) | Critical | **Fixed** (v4.3.2, internal key) |
141
+ | CORS restricted to single origin | Informational | Accepted |
142
+ | Rate limiter bypassable via multiple API keys | Medium | **Open** (recommend per‑tier limits) |
143
+ | No brute‑force protection on API key validation | Medium | **Open** (gateway does not track failed attempts) |
144
+
145
+ ---
146
+
147
+ ## 6. Residual Risk Matrix
148
+
149
+ | Risk | Likelihood | Impact | Rating | Mitigation |
150
+ |------|------------|--------|--------|------------|
151
+ | Tier‑agnostic rate limiting | Medium | Low | Low | Implement per‑tier rate limits in gateway. |
152
+ | No brute‑force protection | Low | Medium | Low | Add exponential backoff or account lockout after N failed attempts. |
153
+ | Dependency vulnerabilities (unscanned) | Medium | Medium | Medium | Integrate automated scanning into CI. |
154
+ | No centralized log aggregation | High | Low | Medium | Add Loki or CloudWatch log shipping. |
155
+ | Internal key not enforced in dev mode | Low | High | Low | Ensure production Helm chart requires the key. |
156
+
157
+ ---
158
+
159
+ ## 7. Bayesian Confidence Model
160
+
161
+ We model the platform's security readiness as a Beta distribution over the probability that no critical security vulnerability exists. We start with a weak Beta(1,1) prior and update based on the findings from this assessment.
162
+
163
+ - **Positive evidence (α‑1):** API auth fixed, constant‑time key compare, Ed25519 signatures, hash‑chained logs, salted API key hashing, NetworkPolicy, CORS restriction, rate limiter.
164
+ - **Negative evidence (β‑1):** No brute‑force protection, dependency scanning not integrated, no centralized log alerting, rate limiter not tier‑specific.
165
+
166
+ Posterior: **Beta(9, 5)**. Posterior mean: **0.64**. This represents our current degree of belief that the platform is secure enough for a pilot. The remaining open items would shift this toward Beta(12,5) with mean ~0.71.
167
+
168
+ ---
169
+
170
+ ## 8. Recommendations
171
+
172
+ | Priority | Recommendation | Effort | Impact |
173
+ |----------|---------------|--------|--------|
174
+ | **P0** | Add brute‑force protection to gateway auth (account lockout after 5 failed attempts). | Small | High |
175
+ | **P0** | Implement per‑tier rate limiting in the gateway. | Medium | Medium |
176
+ | **P1** | Integrate `pip‑audit` and `govulncheck` into CI. | Small | Medium |
177
+ | **P1** | Add centralized log aggregation (Loki or CloudWatch). | Medium | Medium |
178
+ | **P2** | Produce a signed SBOM for the governance loop dependencies. | Small | Low |
179
+
180
+ ---
181
+
182
+ ## 9. Conclusion
183
+
184
+ ARF’s security posture is adequate for a controlled pilot deployment in a regulated environment, provided the P0 recommendations are addressed before production. The platform demonstrates strong integrity controls (Ed25519, hash chains) and authentication (salted SHA‑256, internal key). The residual risks are mitigable with relatively low effort.
185
+
186
+ *This document is proprietary and access‑controlled. Distribution is limited to qualified pilots and enterprise customers under written agreement.*
tests/conftest.py CHANGED
@@ -5,6 +5,7 @@ pytest configuration and fixtures for ARF API tests.
5
  from app.core.usage_tracker import enforce_quota, Tier
6
  from app.api.deps import get_db
7
  from app.database.base import Base
 
8
  from app.main import app as fastapi_app
9
  from sqlalchemy.orm import sessionmaker
10
  from sqlalchemy import create_engine
 
5
  from app.core.usage_tracker import enforce_quota, Tier
6
  from app.api.deps import get_db
7
  from app.database.base import Base
8
+ from app.database.models_intents import IntentDB, TenantDB, BetaStateDB, DecisionAuditLogDB # <-- ensure audit table exists
9
  from app.main import app as fastapi_app
10
  from sqlalchemy.orm import sessionmaker
11
  from sqlalchemy import create_engine
tests/test_governance.py CHANGED
@@ -1,6 +1,17 @@
1
  """
2
  Tests for governance endpoints: /api/v1/intents/evaluate
3
  """
 
 
 
 
 
 
 
 
 
 
 
4
 
5
 
6
  def test_evaluate_provision_intent(client):
@@ -16,7 +27,8 @@ def test_evaluate_provision_intent(client):
16
  "provenance": {},
17
  "configuration": {}
18
  }
19
- response = client.post("/api/v1/intents/evaluate", json=payload)
 
20
  assert response.status_code == 200, response.text
21
  data = response.json()
22
  assert "risk_score" in data
@@ -35,7 +47,8 @@ def test_evaluate_grant_access(client):
35
  "provenance": {},
36
  "justification": "test"
37
  }
38
- response = client.post("/api/v1/intents/evaluate", json=payload)
 
39
  assert response.status_code == 200, response.text
40
  data = response.json()
41
  assert "risk_score" in data
@@ -54,7 +67,8 @@ def test_evaluate_deploy_config(client):
54
  "provenance": {},
55
  "configuration": {}
56
  }
57
- response = client.post("/api/v1/intents/evaluate", json=payload)
 
58
  assert response.status_code == 200, response.text
59
  data = response.json()
60
  assert "risk_score" in data
@@ -67,5 +81,35 @@ def test_invalid_intent_type(client):
67
  "requester": "alice",
68
  "provenance": {}
69
  }
70
- response = client.post("/api/v1/intents/evaluate", json=payload)
 
71
  assert response.status_code == 422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Tests for governance endpoints: /api/v1/intents/evaluate
3
  """
4
+ import pytest
5
+ from app.database.models_intents import TenantDB
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def seed_tenant(db_session):
10
+ """Ensure the tenant 'test-tenant' exists before each test."""
11
+ tenant = db_session.query(TenantDB).filter_by(id="test-tenant").first()
12
+ if not tenant:
13
+ db_session.add(TenantDB(id="test-tenant", name="Test Tenant"))
14
+ db_session.commit()
15
 
16
 
17
  def test_evaluate_provision_intent(client):
 
27
  "provenance": {},
28
  "configuration": {}
29
  }
30
+ response = client.post("/api/v1/intents/evaluate", json=payload,
31
+ headers={"X-Tenant-ID": "test-tenant"})
32
  assert response.status_code == 200, response.text
33
  data = response.json()
34
  assert "risk_score" in data
 
47
  "provenance": {},
48
  "justification": "test"
49
  }
50
+ response = client.post("/api/v1/intents/evaluate", json=payload,
51
+ headers={"X-Tenant-ID": "test-tenant"})
52
  assert response.status_code == 200, response.text
53
  data = response.json()
54
  assert "risk_score" in data
 
67
  "provenance": {},
68
  "configuration": {}
69
  }
70
+ response = client.post("/api/v1/intents/evaluate", json=payload,
71
+ headers={"X-Tenant-ID": "test-tenant"})
72
  assert response.status_code == 200, response.text
73
  data = response.json()
74
  assert "risk_score" in data
 
81
  "requester": "alice",
82
  "provenance": {}
83
  }
84
+ response = client.post("/api/v1/intents/evaluate", json=payload,
85
+ headers={"X-Tenant-ID": "test-tenant"})
86
  assert response.status_code == 422
87
+
88
+
89
+ def test_evaluate_with_criticality(client):
90
+ """v4.3.2: criticality is accepted and a context_hash is generated."""
91
+ payload = {
92
+ "intent_type": "provision_resource",
93
+ "environment": "prod",
94
+ "resource_type": "database",
95
+ "region": "eastus",
96
+ "size": "Standard",
97
+ "estimated_cost": 1200,
98
+ "policy_violations": [],
99
+ "requester": "alice",
100
+ "provenance": {},
101
+ "configuration": {},
102
+ "criticality": 0.85
103
+ }
104
+ response = client.post("/api/v1/intents/evaluate", json=payload,
105
+ headers={"X-Tenant-ID": "test-tenant"})
106
+ assert response.status_code == 200, response.text
107
+ data = response.json()
108
+ assert "risk_score" in data
109
+ # The healing_intent dict should contain the new fields.
110
+ healing = data.get("healing_intent", {})
111
+ # criticality is passed through
112
+ assert healing.get("criticality") == 0.85
113
+ # context_hash is computed by the governance loop (a 64‑char hex string)
114
+ ctx_hash = healing.get("context_hash")
115
+ assert isinstance(ctx_hash, str) and len(ctx_hash) == 64
tests/test_healing_endpoint.py CHANGED
@@ -15,7 +15,6 @@ def test_healing_evaluate_endpoint():
15
  "memory_util": 0.90
16
  }
17
  }
18
- response = client.post("/api/v1/healing/evaluate", json=payload)
19
- assert response.status_code == 200, f"Expected 200, got {
20
- response.status_code}: {
21
- response.text}"
 
15
  "memory_util": 0.90
16
  }
17
  }
18
+ response = client.post("/api/v1/healing/evaluate", json=payload,
19
+ headers={"X-Tenant-ID": "test-tenant"})
20
+ assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}"
 
tests/test_integration.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ End‑to‑end integration tests for the ARF governance pipeline.
3
+
4
+ These tests exercise the full path from HTTP request to HealingIntent
5
+ response, validating that every layer – API, governance loop, policy
6
+ engine, risk engine, audit log, and optional skill/criticality features –
7
+ behaves correctly under realistic conditions.
8
+
9
+ v4.3.2: Covers basic evaluation, skill context, criticality, and audit
10
+ trace verification.
11
+ """
12
+ import pytest
13
+ import time
14
+ from app.database.models_intents import TenantDB, DecisionAuditLogDB
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def seed_tenant(db_session):
19
+ """Ensure the tenant 'test-tenant' exists before each test."""
20
+ tenant = db_session.query(TenantDB).filter_by(id="test-tenant").first()
21
+ if not tenant:
22
+ db_session.add(TenantDB(id="test-tenant", name="Test Tenant"))
23
+ db_session.commit()
24
+
25
+
26
+ class TestFullPipeline:
27
+ """End‑to‑end tests for the /intents/evaluate endpoint."""
28
+
29
+ def test_basic_provision_evaluation(self, client):
30
+ """A minimal valid request returns 200 and a well‑formed HealingIntent."""
31
+ payload = {
32
+ "intent_type": "provision_resource",
33
+ "environment": "prod",
34
+ "resource_type": "database",
35
+ "region": "eastus",
36
+ "size": "Standard",
37
+ "estimated_cost": 1200,
38
+ "policy_violations": [],
39
+ "requester": "alice",
40
+ "provenance": {},
41
+ "configuration": {}
42
+ }
43
+ response = client.post(
44
+ "/api/v1/intents/evaluate",
45
+ json=payload,
46
+ headers={"X-Tenant-ID": "test-tenant"},
47
+ )
48
+ assert response.status_code == 200, response.text
49
+ data = response.json()
50
+ # Top‑level fields
51
+ assert "risk_score" in data
52
+ assert "explanation" in data
53
+ assert "deterministic_id" in data
54
+ assert "recommended_action" in data
55
+ assert isinstance(data["risk_score"], float)
56
+ assert 0.0 <= data["risk_score"] <= 1.0
57
+ # HealingIntent contract
58
+ healing = data.get("healing_intent", {})
59
+ assert healing.get("action") is not None
60
+ assert healing.get("component") is not None
61
+ assert "justification" in healing
62
+ assert "confidence" in healing
63
+ assert "version" in healing
64
+ assert healing["version"] == "2.6.0"
65
+ # v4.3.2: context_hash must be present (64 hex chars)
66
+ ctx_hash = healing.get("context_hash")
67
+ assert isinstance(ctx_hash, str) and len(ctx_hash) == 64, (
68
+ f"context_hash missing or invalid: {ctx_hash}"
69
+ )
70
+
71
+ def test_audit_log_written(self, client, db_session):
72
+ """A successful evaluation writes a row to the decision audit log."""
73
+ # Count existing rows for this tenant
74
+ before = (
75
+ db_session.query(DecisionAuditLogDB)
76
+ .filter_by(tenant_id="test-tenant")
77
+ .count()
78
+ )
79
+ payload = {
80
+ "intent_type": "provision_resource",
81
+ "environment": "prod",
82
+ "resource_type": "database",
83
+ "region": "eastus",
84
+ "size": "Standard",
85
+ "estimated_cost": 1200,
86
+ "policy_violations": [],
87
+ "requester": "alice",
88
+ "provenance": {},
89
+ "configuration": {}
90
+ }
91
+ response = client.post(
92
+ "/api/v1/intents/evaluate",
93
+ json=payload,
94
+ headers={"X-Tenant-ID": "test-tenant"},
95
+ )
96
+ assert response.status_code == 200
97
+ # The write_audit_log runs as a background task; give it a moment.
98
+ time.sleep(0.5)
99
+ after = (
100
+ db_session.query(DecisionAuditLogDB)
101
+ .filter_by(tenant_id="test-tenant")
102
+ .count()
103
+ )
104
+ assert after == before + 1, (
105
+ f"Expected one new audit log entry, but count went from "
106
+ f"{before} to {after}"
107
+ )
108
+
109
+ def test_skill_context_injection(self, client):
110
+ """When skill_id is provided, the response includes skill posterior data."""
111
+ payload = {
112
+ "intent_type": "provision_resource",
113
+ "environment": "prod",
114
+ "resource_type": "database",
115
+ "region": "eastus",
116
+ "size": "Standard",
117
+ "estimated_cost": 1200,
118
+ "policy_violations": [],
119
+ "requester": "alice",
120
+ "provenance": {},
121
+ "configuration": {},
122
+ "skill_id": "pdf-skill",
123
+ }
124
+ response = client.post(
125
+ "/api/v1/intents/evaluate",
126
+ json=payload,
127
+ headers={"X-Tenant-ID": "test-tenant"},
128
+ )
129
+ assert response.status_code == 200
130
+ healing = response.json().get("healing_intent", {})
131
+ # Skill fields should be present in the HealingIntent
132
+ assert "skill_id" in healing
133
+ assert healing["skill_id"] == "pdf-skill"
134
+ # Because the skill registry is a singleton, the skill may or may not
135
+ # already exist. In either case, the fields are populated with either
136
+ # the posterior or the default prior.
137
+ assert "skill_alpha" in healing
138
+ assert "skill_beta" in healing
139
+ assert "skill_reliability_score" in healing
140
+ assert "skill_version" in healing
141
+
142
+ def test_criticality_parameter(self, client):
143
+ """The criticality field is accepted and flows into the HealingIntent."""
144
+ payload = {
145
+ "intent_type": "provision_resource",
146
+ "environment": "prod",
147
+ "resource_type": "database",
148
+ "region": "eastus",
149
+ "size": "Standard",
150
+ "estimated_cost": 1200,
151
+ "policy_violations": [],
152
+ "requester": "alice",
153
+ "provenance": {},
154
+ "configuration": {},
155
+ "criticality": 0.85,
156
+ }
157
+ response = client.post(
158
+ "/api/v1/intents/evaluate",
159
+ json=payload,
160
+ headers={"X-Tenant-ID": "test-tenant"},
161
+ )
162
+ assert response.status_code == 200
163
+ healing = response.json().get("healing_intent", {})
164
+ assert healing.get("criticality") == 0.85, (
165
+ f"criticality should be 0.85, got {healing.get('criticality')}"
166
+ )
167
+
168
+ def test_policy_violation_denial(self, client):
169
+ """An intent with a policy violation returns DENY."""
170
+ payload = {
171
+ "intent_type": "provision_resource",
172
+ "environment": "prod",
173
+ "resource_type": "database",
174
+ "region": "westus", # not in default allowed set
175
+ "size": "Standard",
176
+ "estimated_cost": 1200,
177
+ "policy_violations": ["Region 'westus' not allowed"], # pre‑computed
178
+ "requester": "alice",
179
+ "provenance": {},
180
+ "configuration": {}
181
+ }
182
+ response = client.post(
183
+ "/api/v1/intents/evaluate",
184
+ json=payload,
185
+ headers={"X-Tenant-ID": "test-tenant"},
186
+ )
187
+ assert response.status_code == 200
188
+ data = response.json()
189
+ assert data.get("recommended_action") == "deny", (
190
+ f"Expected action=deny, got {data.get('recommended_action')}"
191
+ )
192
+
193
+
194
+ class TestHealingPipeline:
195
+ """End‑to‑end tests for the /healing/evaluate endpoint."""
196
+
197
+ def test_basic_healing_evaluation(self, client):
198
+ """A reliability event triggers candidate healing actions."""
199
+ payload = {
200
+ "event": {
201
+ "component": "checkout-service",
202
+ "latency_p99": 600.0,
203
+ "error_rate": 0.25,
204
+ "service_mesh": "default",
205
+ "cpu_util": 0.85,
206
+ "memory_util": 0.90,
207
+ }
208
+ }
209
+ response = client.post(
210
+ "/api/v1/healing/evaluate",
211
+ json=payload,
212
+ headers={"X-Tenant-ID": "test-tenant"},
213
+ )
214
+ assert response.status_code == 200
215
+ data = response.json()
216
+ assert "selected_action" in data
217
+ assert data["selected_action"] != "NO_ACTION", (
218
+ "Expected at least one healing action to be triggered"
219
+ )
220
+
221
+ def test_healing_with_skill_context(self, client):
222
+ """Skill context biases the healing decision utility."""
223
+ payload = {
224
+ "event": {
225
+ "component": "checkout-service",
226
+ "latency_p99": 600.0,
227
+ "error_rate": 0.25,
228
+ "service_mesh": "default",
229
+ "cpu_util": 0.85,
230
+ "memory_util": 0.90,
231
+ },
232
+ "skill_id": "pdf-skill",
233
+ "skill_version": 1,
234
+ }
235
+ response = client.post(
236
+ "/api/v1/healing/evaluate",
237
+ json=payload,
238
+ headers={"X-Tenant-ID": "test-tenant"},
239
+ )
240
+ assert response.status_code == 200
241
+ data = response.json()
242
+ # The response should echo the skill context back
243
+ assert data.get("skill_id") == "pdf-skill"
244
+ assert data.get("skill_version") == 1
245
+ assert "selected_action" in data
246
+
247
+
248
+ class TestOutcomeRecording:
249
+ """End‑to‑end tests for the /intents/outcome endpoint."""
250
+
251
+ def test_record_outcome_updates_risk_engine(self, client, db_session):
252
+ """Recording a successful outcome for a previously evaluated intent
253
+ updates the conjugate posterior and skill registry."""
254
+ # Step 1: evaluate an intent to create a record
255
+ payload = {
256
+ "intent_type": "provision_resource",
257
+ "environment": "prod",
258
+ "resource_type": "database",
259
+ "region": "eastus",
260
+ "size": "Standard",
261
+ "estimated_cost": 1200,
262
+ "policy_violations": [],
263
+ "requester": "alice",
264
+ "provenance": {},
265
+ "configuration": {},
266
+ }
267
+ eval_resp = client.post(
268
+ "/api/v1/intents/evaluate",
269
+ json=payload,
270
+ headers={"X-Tenant-ID": "test-tenant"},
271
+ )
272
+ assert eval_resp.status_code == 200
273
+ deterministic_id = eval_resp.json()["deterministic_id"]
274
+
275
+ # Step 2: record a successful outcome
276
+ outcome_payload = {
277
+ "deterministic_id": deterministic_id,
278
+ "success": True,
279
+ "recorded_by": "tester",
280
+ "notes": "integration test",
281
+ }
282
+ outcome_resp = client.post(
283
+ "/api/v1/intents/outcome",
284
+ json=outcome_payload,
285
+ headers={"X-Tenant-ID": "test-tenant"},
286
+ )
287
+ assert outcome_resp.status_code == 200
288
+ assert "outcome_id" in outcome_resp.json()
289
+
290
+ # Step 3: verify that the outcome row exists in the database
291
+ from app.database.models_intents import OutcomeDB
292
+ outcome = (
293
+ db_session.query(OutcomeDB)
294
+ .filter_by(idempotency_key=None) # we didn't send one
295
+ .order_by(OutcomeDB.id.desc())
296
+ .first()
297
+ )
298
+ assert outcome is not None
299
+ assert outcome.success is True
300
+ assert outcome.recorded_by == "tester"
tests/test_intent_store.py CHANGED
@@ -21,11 +21,12 @@ def test_save_intent(db_session):
21
  saved = save_evaluated_intent(
22
  db=db_session,
23
  deterministic_id=det_id,
 
24
  intent_type="ProvisionResourceIntent",
25
  api_payload={"foo": "bar"},
26
  oss_payload={"intent_type": "provision_resource"},
27
  environment="prod",
28
- risk_score=0.42
29
  )
30
  assert saved.deterministic_id == det_id
31
  assert saved.risk_score == "0.42"
@@ -37,9 +38,9 @@ def test_save_intent(db_session):
37
 
38
  def test_update_existing_intent(db_session):
39
  det_id = "intent_123"
40
- save_evaluated_intent(db_session, det_id, "Type", {}, {}, "prod", 0.5)
41
- updated = save_evaluated_intent(
42
- db_session, det_id, "Type", {}, {}, "prod", 0.7)
43
  assert updated.risk_score == "0.7"
44
  count = db_session.query(IntentDB).filter(
45
  IntentDB.deterministic_id == det_id).count()
 
21
  saved = save_evaluated_intent(
22
  db=db_session,
23
  deterministic_id=det_id,
24
+ tenant_id="test-tenant",
25
  intent_type="ProvisionResourceIntent",
26
  api_payload={"foo": "bar"},
27
  oss_payload={"intent_type": "provision_resource"},
28
  environment="prod",
29
+ risk_score=0.42,
30
  )
31
  assert saved.deterministic_id == det_id
32
  assert saved.risk_score == "0.42"
 
38
 
39
  def test_update_existing_intent(db_session):
40
  det_id = "intent_123"
41
+ # Positional order: db, deterministic_id, tenant_id, intent_type, api_payload, oss_payload, environment, risk_score
42
+ save_evaluated_intent(db_session, det_id, "test-tenant", "Type", {}, {}, "prod", 0.5)
43
+ updated = save_evaluated_intent(db_session, det_id, "test-tenant", "Type", {}, {}, "prod", 0.7)
44
  assert updated.risk_score == "0.7"
45
  count = db_session.query(IntentDB).filter(
46
  IntentDB.deterministic_id == det_id).count()
tests/test_outcome_service.py CHANGED
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock
4
  from sqlalchemy import create_engine
5
  from sqlalchemy.orm import sessionmaker
6
  from app.database.base import Base
7
- from app.database.models_intents import IntentDB
8
  from app.services.outcome_service import record_outcome, OutcomeConflictError
9
  from agentic_reliability_framework.core.governance.intents import (
10
  ProvisionResourceIntent,
@@ -18,6 +18,10 @@ def db_session():
18
  TestingSessionLocal = sessionmaker(bind=engine, future=True)
19
  Base.metadata.create_all(bind=engine)
20
  sess = TestingSessionLocal()
 
 
 
 
21
  yield sess
22
  sess.close()
23
 
@@ -42,6 +46,7 @@ def test_record_outcome_creates_row_and_updates_engine(
42
 
43
  intent = IntentDB(
44
  deterministic_id="intent_abc",
 
45
  intent_type="ProvisionResourceIntent",
46
  payload={},
47
  oss_payload=oss_payload,
@@ -83,6 +88,7 @@ def test_record_outcome_creates_row_and_updates_engine(
83
  def test_conflict_different_result(db_session, mock_risk_engine):
84
  intent = IntentDB(
85
  deterministic_id="intent_def",
 
86
  intent_type="ProvisionResourceIntent",
87
  payload={},
88
  created_at=datetime.datetime.utcnow()
@@ -123,6 +129,7 @@ def test_record_outcome_reconstruction_failure_does_not_update_engine(
123
  # Create an intent with invalid oss_payload (missing required fields)
124
  intent = IntentDB(
125
  deterministic_id="intent_bad",
 
126
  intent_type="ProvisionResourceIntent",
127
  payload={},
128
  oss_payload={"intent_type": "provision_resource"}, # missing fields
 
4
  from sqlalchemy import create_engine
5
  from sqlalchemy.orm import sessionmaker
6
  from app.database.base import Base
7
+ from app.database.models_intents import IntentDB, TenantDB
8
  from app.services.outcome_service import record_outcome, OutcomeConflictError
9
  from agentic_reliability_framework.core.governance.intents import (
10
  ProvisionResourceIntent,
 
18
  TestingSessionLocal = sessionmaker(bind=engine, future=True)
19
  Base.metadata.create_all(bind=engine)
20
  sess = TestingSessionLocal()
21
+ # Ensure a tenant exists for foreign key constraints
22
+ if not sess.query(TenantDB).filter_by(id="test-tenant").first():
23
+ sess.add(TenantDB(id="test-tenant", name="Test Tenant"))
24
+ sess.commit()
25
  yield sess
26
  sess.close()
27
 
 
46
 
47
  intent = IntentDB(
48
  deterministic_id="intent_abc",
49
+ tenant_id="test-tenant", # <-- required
50
  intent_type="ProvisionResourceIntent",
51
  payload={},
52
  oss_payload=oss_payload,
 
88
  def test_conflict_different_result(db_session, mock_risk_engine):
89
  intent = IntentDB(
90
  deterministic_id="intent_def",
91
+ tenant_id="test-tenant", # <-- required
92
  intent_type="ProvisionResourceIntent",
93
  payload={},
94
  created_at=datetime.datetime.utcnow()
 
129
  # Create an intent with invalid oss_payload (missing required fields)
130
  intent = IntentDB(
131
  deterministic_id="intent_bad",
132
+ tenant_id="test-tenant", # <-- required
133
  intent_type="ProvisionResourceIntent",
134
  payload={},
135
  oss_payload={"intent_type": "provision_resource"}, # missing fields
tests/test_performance.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Performance benchmarks for the ARF governance pipeline.
3
+
4
+ These tests measure the latency of key operations and assert that
5
+ they remain within the target thresholds for pilot readiness.
6
+
7
+ Targets (v4.3.2):
8
+ - Full governance loop (single intent): p50 < 50 ms, p99 < 100 ms
9
+ - Policy evaluation alone: p50 < 1 ms
10
+ - Conjugate update: p50 < 0.1 ms
11
+ - HealingIntent serialization: p50 < 5 ms
12
+ """
13
+ import time
14
+ import pytest
15
+ import numpy as np
16
+ from unittest.mock import Mock, patch
17
+
18
+ from agentic_reliability_framework.core.governance.governance_loop import GovernanceLoop
19
+ from agentic_reliability_framework.core.governance.intents import (
20
+ ProvisionResourceIntent,
21
+ ResourceType,
22
+ )
23
+ from agentic_reliability_framework.core.governance.policies import PolicyEvaluator, allow_all
24
+ from agentic_reliability_framework.core.governance.cost_estimator import CostEstimator
25
+ from agentic_reliability_framework.core.governance.risk_engine import RiskEngine
26
+ from agentic_reliability_framework.core.governance.healing_intent import HealingIntent
27
+
28
+
29
+ # Number of warmup iterations and measured iterations
30
+ WARMUP = 10
31
+ MEASURED = 50
32
+
33
+
34
+ def _measure_latency(fn, *args, **kwargs):
35
+ """Run fn MEASURED times after WARMUP warmups, return (p50, p99, p100) in seconds."""
36
+ times = []
37
+ for _ in range(WARMUP):
38
+ fn(*args, **kwargs)
39
+ for _ in range(MEASURED):
40
+ t0 = time.perf_counter()
41
+ fn(*args, **kwargs)
42
+ times.append(time.perf_counter() - t0)
43
+ arr = np.array(times) * 1000 # convert to milliseconds
44
+ return np.percentile(arr, 50), np.percentile(arr, 99), arr.max()
45
+
46
+
47
+ @pytest.fixture(scope="module")
48
+ def sample_intent():
49
+ return ProvisionResourceIntent(
50
+ resource_type=ResourceType.VM,
51
+ region="eastus",
52
+ size="Standard_D2s_v3",
53
+ requester="perf-test",
54
+ environment="dev",
55
+ )
56
+
57
+
58
+ @pytest.fixture(scope="module")
59
+ def governance_loop():
60
+ return GovernanceLoop(
61
+ policy_evaluator=PolicyEvaluator(allow_all()),
62
+ cost_estimator=CostEstimator(),
63
+ risk_engine=RiskEngine(),
64
+ enable_epistemic=False,
65
+ )
66
+
67
+
68
+ class TestGovernanceLoopPerformance:
69
+ """Latency benchmarks for the full governance loop."""
70
+
71
+ def test_full_loop_latency(self, governance_loop, sample_intent):
72
+ """The full loop should complete within 100 ms at p99."""
73
+ p50, p99, p100 = _measure_latency(
74
+ governance_loop.run, sample_intent, context={"service_name": "perf-svc"}
75
+ )
76
+ assert p50 < 100, f"p50 latency {p50:.1f} ms exceeds 100 ms target"
77
+ assert p99 < 200, f"p99 latency {p99:.1f} ms exceeds 200 ms target"
78
+
79
+
80
+ class TestHealingIntentSerialization:
81
+ """Serialization performance."""
82
+
83
+ def test_to_enterprise_request_latency(self, governance_loop, sample_intent):
84
+ """Serializing a HealingIntent to the enterprise request dict should be fast."""
85
+ intent = governance_loop.run(sample_intent, context={"service_name": "perf-svc"})
86
+ p50, p99, p100 = _measure_latency(intent.to_enterprise_request)
87
+ assert p50 < 10, f"p50 serialization latency {p50:.1f} ms exceeds 10 ms target"
88
+
89
+
90
+ class TestRiskEnginePerformance:
91
+ """Conjugate update latency."""
92
+
93
+ def test_risk_calculation_latency(self, governance_loop, sample_intent):
94
+ """A single risk calculation should be sub‑millisecond."""
95
+ engine = governance_loop.risk_engine
96
+ p50, p99, p100 = _measure_latency(
97
+ engine.calculate_risk,
98
+ intent=sample_intent,
99
+ cost_estimate=None,
100
+ policy_violations=[],
101
+ )
102
+ assert p50 < 10, f"p50 risk calculation latency {p50:.1f} ms exceeds 10 ms target"
tests/test_usage_tracker.py CHANGED
@@ -11,14 +11,15 @@ def tracker():
11
 
12
 
13
  def test_get_or_create_api_key(tracker):
14
- assert tracker.get_or_create_api_key("test_key", Tier.FREE) is True
 
15
  assert tracker.get_tier("test_key") == Tier.FREE
16
  # Second call should return True without error
17
- assert tracker.get_or_create_api_key("test_key") is True
18
 
19
 
20
  def test_update_api_key_tier(tracker):
21
- tracker.get_or_create_api_key("test_key", Tier.FREE)
22
  assert tracker.update_api_key_tier("test_key", Tier.PRO) is True
23
  assert tracker.get_tier("test_key") == Tier.PRO
24
  # Non-existent key
@@ -26,7 +27,7 @@ def test_update_api_key_tier(tracker):
26
 
27
 
28
  def test_get_remaining_quota_free(tracker):
29
- tracker.get_or_create_api_key("free_key", Tier.FREE)
30
  # Initially 1000 remaining
31
  remaining = tracker.get_remaining_quota("free_key", Tier.FREE)
32
  assert remaining == 1000
@@ -43,13 +44,13 @@ def test_get_remaining_quota_free(tracker):
43
 
44
 
45
  def test_get_remaining_quota_enterprise(tracker):
46
- tracker.get_or_create_api_key("ent_key", Tier.ENTERPRISE)
47
  remaining = tracker.get_remaining_quota("ent_key", Tier.ENTERPRISE)
48
  assert remaining is None
49
 
50
 
51
  def test_increment_usage_sync(tracker):
52
- tracker.get_or_create_api_key("test_key", Tier.FREE)
53
  record = UsageRecord(
54
  api_key="test_key",
55
  tier=Tier.FREE,
@@ -64,7 +65,7 @@ def test_increment_usage_sync(tracker):
64
 
65
 
66
  def test_get_audit_logs(tracker):
67
- tracker.get_or_create_api_key("test_key", Tier.FREE)
68
  record = UsageRecord(
69
  api_key="test_key",
70
  tier=Tier.FREE,
 
11
 
12
 
13
  def test_get_or_create_api_key(tracker):
14
+ # Updated: pass tenant_id as keyword argument (new signature)
15
+ assert tracker.get_or_create_api_key("test_key", tenant_id="test") is True
16
  assert tracker.get_tier("test_key") == Tier.FREE
17
  # Second call should return True without error
18
+ assert tracker.get_or_create_api_key("test_key", tenant_id="test") is True
19
 
20
 
21
  def test_update_api_key_tier(tracker):
22
+ tracker.get_or_create_api_key("test_key", tenant_id="test")
23
  assert tracker.update_api_key_tier("test_key", Tier.PRO) is True
24
  assert tracker.get_tier("test_key") == Tier.PRO
25
  # Non-existent key
 
27
 
28
 
29
  def test_get_remaining_quota_free(tracker):
30
+ tracker.get_or_create_api_key("free_key", tenant_id="test")
31
  # Initially 1000 remaining
32
  remaining = tracker.get_remaining_quota("free_key", Tier.FREE)
33
  assert remaining == 1000
 
44
 
45
 
46
  def test_get_remaining_quota_enterprise(tracker):
47
+ tracker.get_or_create_api_key("ent_key", tenant_id="test")
48
  remaining = tracker.get_remaining_quota("ent_key", Tier.ENTERPRISE)
49
  assert remaining is None
50
 
51
 
52
  def test_increment_usage_sync(tracker):
53
+ tracker.get_or_create_api_key("test_key", tenant_id="test")
54
  record = UsageRecord(
55
  api_key="test_key",
56
  tier=Tier.FREE,
 
65
 
66
 
67
  def test_get_audit_logs(tracker):
68
+ tracker.get_or_create_api_key("test_key", tenant_id="test")
69
  record = UsageRecord(
70
  api_key="test_key",
71
  tier=Tier.FREE,