agent_hypervisor 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. agent_hypervisor-3.1.0.dist-info/METADATA +824 -0
  2. agent_hypervisor-3.1.0.dist-info/RECORD +60 -0
  3. agent_hypervisor-3.1.0.dist-info/WHEEL +4 -0
  4. agent_hypervisor-3.1.0.dist-info/entry_points.txt +2 -0
  5. agent_hypervisor-3.1.0.dist-info/licenses/LICENSE +21 -0
  6. hypervisor/__init__.py +160 -0
  7. hypervisor/api/__init__.py +7 -0
  8. hypervisor/api/models.py +285 -0
  9. hypervisor/api/server.py +742 -0
  10. hypervisor/audit/__init__.py +4 -0
  11. hypervisor/audit/commitment.py +76 -0
  12. hypervisor/audit/delta.py +135 -0
  13. hypervisor/audit/gc.py +99 -0
  14. hypervisor/cli/__init__.py +3 -0
  15. hypervisor/cli/formatters.py +99 -0
  16. hypervisor/cli/session_commands.py +200 -0
  17. hypervisor/constants.py +106 -0
  18. hypervisor/core.py +352 -0
  19. hypervisor/integrations/__init__.py +10 -0
  20. hypervisor/integrations/iatp_adapter.py +142 -0
  21. hypervisor/integrations/nexus_adapter.py +108 -0
  22. hypervisor/integrations/verification_adapter.py +122 -0
  23. hypervisor/liability/__init__.py +142 -0
  24. hypervisor/liability/attribution.py +86 -0
  25. hypervisor/liability/ledger.py +121 -0
  26. hypervisor/liability/quarantine.py +119 -0
  27. hypervisor/liability/slashing.py +80 -0
  28. hypervisor/liability/vouching.py +134 -0
  29. hypervisor/models.py +277 -0
  30. hypervisor/observability/__init__.py +27 -0
  31. hypervisor/observability/causal_trace.py +70 -0
  32. hypervisor/observability/event_bus.py +222 -0
  33. hypervisor/observability/prometheus_collector.py +248 -0
  34. hypervisor/observability/saga_span_exporter.py +341 -0
  35. hypervisor/providers.py +121 -0
  36. hypervisor/py.typed +0 -0
  37. hypervisor/reversibility/__init__.py +3 -0
  38. hypervisor/reversibility/registry.py +108 -0
  39. hypervisor/rings/__init__.py +21 -0
  40. hypervisor/rings/breach_detector.py +200 -0
  41. hypervisor/rings/classifier.py +78 -0
  42. hypervisor/rings/elevation.py +219 -0
  43. hypervisor/rings/enforcer.py +97 -0
  44. hypervisor/saga/__init__.py +22 -0
  45. hypervisor/saga/checkpoint.py +110 -0
  46. hypervisor/saga/dsl.py +190 -0
  47. hypervisor/saga/fan_out.py +126 -0
  48. hypervisor/saga/orchestrator.py +229 -0
  49. hypervisor/saga/schema.py +244 -0
  50. hypervisor/saga/state_machine.py +157 -0
  51. hypervisor/security/__init__.py +13 -0
  52. hypervisor/security/kill_switch.py +200 -0
  53. hypervisor/security/rate_limiter.py +190 -0
  54. hypervisor/session/__init__.py +194 -0
  55. hypervisor/session/intent_locks.py +118 -0
  56. hypervisor/session/isolation.py +37 -0
  57. hypervisor/session/sso.py +169 -0
  58. hypervisor/session/vector_clock.py +118 -0
  59. hypervisor/verification/__init__.py +3 -0
  60. hypervisor/verification/history.py +173 -0
@@ -0,0 +1,742 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """
4
+ FastAPI REST API server for the Agent Hypervisor.
5
+
6
+ Exposes the hypervisor's core capabilities — sessions, rings, sagas,
7
+ liability, events, and health — as a RESTful API with OpenAPI docs.
8
+
9
+ Run with: uvicorn hypervisor.api.server:app
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ from contextlib import asynccontextmanager
17
+ from typing import Any
18
+
19
+ from fastapi import FastAPI, HTTPException, Query
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+
22
+ from hypervisor import __version__
23
+ from hypervisor.api.models import (
24
+ AddStepRequest,
25
+ AddStepResponse,
26
+ AgentRingResponse,
27
+ CommitmentResponse,
28
+ CreateSagaResponse,
29
+ CreateSessionRequest,
30
+ CreateSessionResponse,
31
+ CreateVouchRequest,
32
+ EventResponse,
33
+ EventStatsResponse,
34
+ ExecuteStepResponse,
35
+ JoinSessionRequest,
36
+ JoinSessionResponse,
37
+ LiabilityExposureResponse,
38
+ ParticipantInfo,
39
+ RingCheckRequest,
40
+ RingCheckResponse,
41
+ RingDistributionResponse,
42
+ SagaDetailResponse,
43
+ SessionDetailResponse,
44
+ SessionListItem,
45
+ StatsResponse,
46
+ VerifyCommitmentResponse,
47
+ VerifyHistoryRequest,
48
+ VerifyHistoryResponse,
49
+ VouchResponse,
50
+ )
51
+ from hypervisor.core import Hypervisor, ManagedSession
52
+ from hypervisor.models import (
53
+ ActionDescriptor,
54
+ ExecutionRing,
55
+ SessionConfig,
56
+ )
57
+ from hypervisor.observability.event_bus import EventType, HypervisorEventBus
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+ # ── Global state ────────────────────────────────────────────────────────────
62
+
63
+ _hypervisor: Hypervisor | None = None
64
+ _event_bus: HypervisorEventBus | None = None
65
+
66
+
67
+ def _hv() -> Hypervisor:
68
+ """Get the global Hypervisor instance."""
69
+ if _hypervisor is None:
70
+ raise HTTPException(status_code=503, detail="Hypervisor not initialized")
71
+ return _hypervisor
72
+
73
+
74
+ def _bus() -> HypervisorEventBus:
75
+ """Get the global event bus."""
76
+ if _event_bus is None:
77
+ raise HTTPException(status_code=503, detail="Event bus not initialized")
78
+ return _event_bus
79
+
80
+
81
+ def _get_managed(session_id: str) -> ManagedSession:
82
+ """Get a managed session or raise 404."""
83
+ managed = _hv().get_session(session_id)
84
+ if not managed:
85
+ raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
86
+ return managed
87
+
88
+
89
+ def _participant_info(p: Any) -> ParticipantInfo:
90
+ return ParticipantInfo(
91
+ agent_did=p.agent_did,
92
+ ring=p.ring.value,
93
+ sigma_raw=p.sigma_raw,
94
+ eff_score=p.eff_score,
95
+ joined_at=p.joined_at.isoformat(),
96
+ is_active=p.is_active,
97
+ )
98
+
99
+
100
+ # ── Lifespan ────────────────────────────────────────────────────────────────
101
+
102
+ @asynccontextmanager
103
+ async def lifespan(app: FastAPI): # type: ignore[no-untyped-def]
104
+ """Initialize hypervisor on startup, clean up on shutdown."""
105
+ global _hypervisor, _event_bus
106
+ _hypervisor = Hypervisor()
107
+ _event_bus = HypervisorEventBus()
108
+ yield
109
+ _hypervisor = None
110
+ _event_bus = None
111
+
112
+
113
+ # ── App factory ─────────────────────────────────────────────────────────────
114
+
115
+ def create_app() -> FastAPI:
116
+ """Create and configure the FastAPI application."""
117
+ application = FastAPI(
118
+ title="Agent Hypervisor API",
119
+ description=(
120
+ "REST API for the Agent Hypervisor — runtime supervisor for "
121
+ "multi-agent Shared Sessions with Execution Rings, Joint Liability, "
122
+ "Saga Orchestration, and Audit log audit trails."
123
+ ),
124
+ version=__version__,
125
+ lifespan=lifespan,
126
+ )
127
+
128
+ application.add_middleware(
129
+ CORSMiddleware,
130
+ allow_origins=os.environ.get("CORS_ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:8080").split(","),
131
+ allow_credentials=True,
132
+ allow_methods=["*"],
133
+ allow_headers=["*"],
134
+ )
135
+
136
+ return application
137
+
138
+
139
+ app = create_app()
140
+
141
+
142
+ # ── Health ──────────────────────────────────────────────────────────────────
143
+
144
+ @app.get("/health", tags=["Health"])
145
+ async def health_check() -> dict[str, str]:
146
+ """Health check endpoint."""
147
+ return {"status": "ok", "version": __version__}
148
+
149
+
150
+ @app.get("/api/v1/stats", response_model=StatsResponse, tags=["Health"])
151
+ async def get_stats() -> StatsResponse:
152
+ """Get overall hypervisor statistics."""
153
+ hv = _hv()
154
+ bus = _bus()
155
+ total_participants = sum(
156
+ m.sso.participant_count for m in hv._sessions.values()
157
+ )
158
+ active_sagas = sum(
159
+ len(m.saga.active_sagas) for m in hv._sessions.values()
160
+ )
161
+ return StatsResponse(
162
+ version=__version__,
163
+ total_sessions=len(hv._sessions),
164
+ active_sessions=len(hv.active_sessions),
165
+ total_participants=total_participants,
166
+ active_sagas=active_sagas,
167
+ total_vouches=len(hv.vouching._vouches),
168
+ event_count=bus.event_count,
169
+ )
170
+
171
+
172
+ # ── Sessions ────────────────────────────────────────────────────────────────
173
+
174
+ @app.post(
175
+ "/api/v1/sessions",
176
+ response_model=CreateSessionResponse,
177
+ status_code=201,
178
+ tags=["Sessions"],
179
+ )
180
+ async def create_session(req: CreateSessionRequest) -> CreateSessionResponse:
181
+ """Create a new Shared Session."""
182
+ config = SessionConfig(
183
+ consistency_mode=req.consistency_mode,
184
+ max_participants=req.max_participants,
185
+ max_duration_seconds=req.max_duration_seconds,
186
+ min_eff_score=req.min_eff_score,
187
+ enable_audit=req.enable_audit,
188
+ enable_blockchain_commitment=req.enable_blockchain_commitment,
189
+ )
190
+ managed = await _hv().create_session(config=config, creator_did=req.creator_did)
191
+ return CreateSessionResponse(
192
+ session_id=managed.sso.session_id,
193
+ state=managed.sso.state.value,
194
+ consistency_mode=managed.sso.consistency_mode.value,
195
+ created_at=managed.sso.created_at.isoformat(),
196
+ )
197
+
198
+
199
+ @app.get("/api/v1/sessions", response_model=list[SessionListItem], tags=["Sessions"])
200
+ async def list_sessions(
201
+ state: str | None = Query(None, description="Filter by session state"),
202
+ ) -> list[SessionListItem]:
203
+ """List all sessions, optionally filtered by state."""
204
+ sessions = _hv()._sessions.values()
205
+ if state:
206
+ sessions = [m for m in sessions if m.sso.state.value == state]
207
+ return [
208
+ SessionListItem(
209
+ session_id=m.sso.session_id,
210
+ state=m.sso.state.value,
211
+ consistency_mode=m.sso.consistency_mode.value,
212
+ participant_count=m.sso.participant_count,
213
+ created_at=m.sso.created_at.isoformat(),
214
+ )
215
+ for m in sessions
216
+ ]
217
+
218
+
219
+ @app.get(
220
+ "/api/v1/sessions/{session_id}",
221
+ response_model=SessionDetailResponse,
222
+ tags=["Sessions"],
223
+ )
224
+ async def get_session(session_id: str) -> SessionDetailResponse:
225
+ """Get detailed session information including participants and sagas."""
226
+ managed = _get_managed(session_id)
227
+ sso = managed.sso
228
+ sagas = [
229
+ s.to_dict()
230
+ for s in managed.saga._sagas.values()
231
+ ]
232
+ return SessionDetailResponse(
233
+ session_id=sso.session_id,
234
+ state=sso.state.value,
235
+ consistency_mode=sso.consistency_mode.value,
236
+ creator_did=sso.creator_did,
237
+ participant_count=sso.participant_count,
238
+ participants=[_participant_info(p) for p in sso.participants],
239
+ created_at=sso.created_at.isoformat(),
240
+ terminated_at=sso.terminated_at.isoformat() if sso.terminated_at else None,
241
+ sagas=sagas,
242
+ )
243
+
244
+
245
+ @app.post(
246
+ "/api/v1/sessions/{session_id}/join",
247
+ response_model=JoinSessionResponse,
248
+ tags=["Sessions"],
249
+ )
250
+ async def join_session(session_id: str, req: JoinSessionRequest) -> JoinSessionResponse:
251
+ """Join an agent to a session via the IATP handshake."""
252
+ hv = _hv()
253
+ actions = None
254
+ if req.actions:
255
+ actions = [ActionDescriptor(**a) for a in req.actions]
256
+ try:
257
+ ring = await hv.join_session(
258
+ session_id=session_id,
259
+ agent_did=req.agent_did,
260
+ actions=actions,
261
+ sigma_raw=req.sigma_raw,
262
+ )
263
+ except ValueError as e:
264
+ raise HTTPException(status_code=404, detail=str(e))
265
+ except Exception as e:
266
+ logger.debug("join_session failed for %s: %s", session_id, e, exc_info=True)
267
+ raise HTTPException(status_code=400, detail=str(e))
268
+ return JoinSessionResponse(
269
+ agent_did=req.agent_did,
270
+ session_id=session_id,
271
+ assigned_ring=ring.value,
272
+ ring_name=ring.name,
273
+ )
274
+
275
+
276
+ @app.post("/api/v1/sessions/{session_id}/activate", tags=["Sessions"])
277
+ async def activate_session(session_id: str) -> dict[str, str]:
278
+ """Activate a session after handshaking is complete."""
279
+ try:
280
+ await _hv().activate_session(session_id)
281
+ except ValueError as e:
282
+ raise HTTPException(status_code=404, detail=str(e))
283
+ except Exception as e:
284
+ logger.debug("activate_session failed for %s: %s", session_id, e, exc_info=True)
285
+ raise HTTPException(status_code=400, detail=str(e))
286
+ return {"session_id": session_id, "state": "active"}
287
+
288
+
289
+ @app.post("/api/v1/sessions/{session_id}/terminate", tags=["Sessions"])
290
+ async def terminate_session(session_id: str) -> dict[str, Any]:
291
+ """Terminate a session and commit audit trail."""
292
+ try:
293
+ hash_chain_root = await _hv().terminate_session(session_id)
294
+ except ValueError as e:
295
+ raise HTTPException(status_code=404, detail=str(e))
296
+ except Exception as e:
297
+ logger.debug("terminate_session failed for %s: %s", session_id, e, exc_info=True)
298
+ raise HTTPException(status_code=400, detail=str(e))
299
+ return {
300
+ "session_id": session_id,
301
+ "state": "archived",
302
+ "hash_chain_root": hash_chain_root,
303
+ }
304
+
305
+
306
+ # ── Rings ───────────────────────────────────────────────────────────────────
307
+
308
+ @app.get(
309
+ "/api/v1/sessions/{session_id}/rings",
310
+ response_model=RingDistributionResponse,
311
+ tags=["Rings"],
312
+ )
313
+ async def get_ring_distribution(session_id: str) -> RingDistributionResponse:
314
+ """Get the ring distribution for all participants in a session."""
315
+ managed = _get_managed(session_id)
316
+ distribution: dict[str, list[str]] = {}
317
+ for p in managed.sso.participants:
318
+ ring_name = p.ring.name
319
+ distribution.setdefault(ring_name, []).append(p.agent_did)
320
+ return RingDistributionResponse(session_id=session_id, distribution=distribution)
321
+
322
+
323
+ @app.get(
324
+ "/api/v1/agents/{agent_did}/ring",
325
+ response_model=AgentRingResponse,
326
+ tags=["Rings"],
327
+ )
328
+ async def get_agent_ring(agent_did: str) -> AgentRingResponse:
329
+ """Get an agent's current ring level across all active sessions."""
330
+ hv = _hv()
331
+ for managed in hv._sessions.values():
332
+ for p in managed.sso.participants:
333
+ if p.agent_did == agent_did and p.is_active:
334
+ return AgentRingResponse(
335
+ agent_did=agent_did,
336
+ ring=p.ring.value,
337
+ ring_name=p.ring.name,
338
+ session_id=managed.sso.session_id,
339
+ )
340
+ raise HTTPException(status_code=404, detail=f"Agent {agent_did} not found in any session")
341
+
342
+
343
+ @app.post(
344
+ "/api/v1/rings/check",
345
+ response_model=RingCheckResponse,
346
+ tags=["Rings"],
347
+ )
348
+ async def check_ring_access(req: RingCheckRequest) -> RingCheckResponse:
349
+ """Check if an action is allowed for the given ring level and sigma."""
350
+ hv = _hv()
351
+ action = ActionDescriptor(**req.action)
352
+ agent_ring = ExecutionRing(req.agent_ring)
353
+ result = hv.ring_enforcer.check(
354
+ agent_ring=agent_ring,
355
+ action=action,
356
+ eff_score=req.eff_score,
357
+ has_consensus=req.has_consensus,
358
+ has_sre_witness=req.has_sre_witness,
359
+ )
360
+ return RingCheckResponse(
361
+ allowed=result.allowed,
362
+ required_ring=result.required_ring.value,
363
+ agent_ring=result.agent_ring.value,
364
+ eff_score=result.eff_score,
365
+ reason=result.reason,
366
+ requires_consensus=result.requires_consensus,
367
+ requires_sre_witness=result.requires_sre_witness,
368
+ )
369
+
370
+
371
+ # ── Sagas ───────────────────────────────────────────────────────────────────
372
+
373
+ @app.post(
374
+ "/api/v1/sessions/{session_id}/sagas",
375
+ response_model=CreateSagaResponse,
376
+ status_code=201,
377
+ tags=["Sagas"],
378
+ )
379
+ async def create_saga(session_id: str) -> CreateSagaResponse:
380
+ """Create a new saga for a session."""
381
+ managed = _get_managed(session_id)
382
+ saga = managed.saga.create_saga(session_id)
383
+ return CreateSagaResponse(
384
+ saga_id=saga.saga_id,
385
+ session_id=saga.session_id,
386
+ state=saga.state.value,
387
+ created_at=saga.created_at.isoformat(),
388
+ )
389
+
390
+
391
+ @app.get(
392
+ "/api/v1/sessions/{session_id}/sagas",
393
+ response_model=list[SagaDetailResponse],
394
+ tags=["Sagas"],
395
+ )
396
+ async def list_sagas(session_id: str) -> list[SagaDetailResponse]:
397
+ """List all sagas in a session."""
398
+ managed = _get_managed(session_id)
399
+ return [
400
+ SagaDetailResponse(
401
+ saga_id=s.saga_id,
402
+ session_id=s.session_id,
403
+ state=s.state.value,
404
+ created_at=s.created_at.isoformat(),
405
+ completed_at=s.completed_at.isoformat() if s.completed_at else None,
406
+ error=s.error,
407
+ steps=[
408
+ {
409
+ "step_id": st.step_id,
410
+ "action_id": st.action_id,
411
+ "agent_did": st.agent_did,
412
+ "state": st.state.value,
413
+ "error": st.error,
414
+ }
415
+ for st in s.steps
416
+ ],
417
+ )
418
+ for s in managed.saga._sagas.values()
419
+ ]
420
+
421
+
422
+ @app.get(
423
+ "/api/v1/sagas/{saga_id}",
424
+ response_model=SagaDetailResponse,
425
+ tags=["Sagas"],
426
+ )
427
+ async def get_saga(saga_id: str) -> SagaDetailResponse:
428
+ """Get detailed saga information including steps and state."""
429
+ hv = _hv()
430
+ for managed in hv._sessions.values():
431
+ saga = managed.saga.get_saga(saga_id)
432
+ if saga:
433
+ return SagaDetailResponse(
434
+ saga_id=saga.saga_id,
435
+ session_id=saga.session_id,
436
+ state=saga.state.value,
437
+ created_at=saga.created_at.isoformat(),
438
+ completed_at=saga.completed_at.isoformat() if saga.completed_at else None,
439
+ error=saga.error,
440
+ steps=[
441
+ {
442
+ "step_id": st.step_id,
443
+ "action_id": st.action_id,
444
+ "agent_did": st.agent_did,
445
+ "state": st.state.value,
446
+ "error": st.error,
447
+ }
448
+ for st in saga.steps
449
+ ],
450
+ )
451
+ raise HTTPException(status_code=404, detail=f"Saga {saga_id} not found")
452
+
453
+
454
+ @app.post(
455
+ "/api/v1/sagas/{saga_id}/steps",
456
+ response_model=AddStepResponse,
457
+ status_code=201,
458
+ tags=["Sagas"],
459
+ )
460
+ async def add_saga_step(saga_id: str, req: AddStepRequest) -> AddStepResponse:
461
+ """Add a step to an existing saga."""
462
+ hv = _hv()
463
+ for managed in hv._sessions.values():
464
+ saga = managed.saga.get_saga(saga_id)
465
+ if saga:
466
+ try:
467
+ step = managed.saga.add_step(
468
+ saga_id=saga_id,
469
+ action_id=req.action_id,
470
+ agent_did=req.agent_did,
471
+ execute_api=req.execute_api,
472
+ undo_api=req.undo_api,
473
+ timeout_seconds=req.timeout_seconds,
474
+ max_retries=req.max_retries,
475
+ )
476
+ except Exception as e:
477
+ logger.debug("add_step failed for saga %s: %s", saga_id, e, exc_info=True)
478
+ raise HTTPException(status_code=400, detail=str(e))
479
+ return AddStepResponse(
480
+ step_id=step.step_id,
481
+ saga_id=saga_id,
482
+ action_id=step.action_id,
483
+ state=step.state.value,
484
+ )
485
+ raise HTTPException(status_code=404, detail=f"Saga {saga_id} not found")
486
+
487
+
488
+ @app.post(
489
+ "/api/v1/sagas/{saga_id}/steps/{step_id}/execute",
490
+ response_model=ExecuteStepResponse,
491
+ tags=["Sagas"],
492
+ )
493
+ async def execute_saga_step(saga_id: str, step_id: str) -> ExecuteStepResponse:
494
+ """Execute a pending saga step (using a no-op executor for API-driven flow)."""
495
+ hv = _hv()
496
+ for managed in hv._sessions.values():
497
+ saga = managed.saga.get_saga(saga_id)
498
+ if saga:
499
+ try:
500
+ async def _noop_executor() -> dict[str, str]:
501
+ return {"status": "executed_via_api"}
502
+
503
+ await managed.saga.execute_step(saga_id, step_id, _noop_executor)
504
+ except Exception as e:
505
+ logger.debug("execute_step failed for saga %s step %s: %s", saga_id, step_id, e, exc_info=True)
506
+ raise HTTPException(status_code=400, detail=str(e))
507
+ # Find the step to return its state
508
+ for st in saga.steps:
509
+ if st.step_id == step_id:
510
+ return ExecuteStepResponse(
511
+ step_id=step_id,
512
+ saga_id=saga_id,
513
+ state=st.state.value,
514
+ error=st.error,
515
+ )
516
+ raise HTTPException(status_code=404, detail=f"Saga {saga_id} or step {step_id} not found")
517
+
518
+
519
+ # ── Liability ───────────────────────────────────────────────────────────────
520
+
521
+ @app.post(
522
+ "/api/v1/sessions/{session_id}/sponsor",
523
+ response_model=VouchResponse,
524
+ status_code=201,
525
+ tags=["Liability"],
526
+ )
527
+ async def create_vouch(session_id: str, req: CreateVouchRequest) -> VouchResponse:
528
+ """Create a sponsorship bond between agents in a session."""
529
+ hv = _hv()
530
+ _get_managed(session_id) # verify session exists
531
+ try:
532
+ record = hv.vouching.vouch(
533
+ voucher_did=req.voucher_did,
534
+ vouchee_did=req.vouchee_did,
535
+ session_id=session_id,
536
+ voucher_sigma=req.voucher_sigma,
537
+ bond_pct=req.bond_pct,
538
+ )
539
+ except Exception as e:
540
+ logger.debug("create_vouch failed for session %s: %s", session_id, e, exc_info=True)
541
+ raise HTTPException(status_code=400, detail=str(e))
542
+ return VouchResponse(
543
+ vouch_id=record.vouch_id,
544
+ voucher_did=record.voucher_did,
545
+ vouchee_did=record.vouchee_did,
546
+ session_id=record.session_id,
547
+ bonded_amount=record.bonded_amount,
548
+ bonded_sigma_pct=record.bonded_sigma_pct,
549
+ is_active=record.is_active,
550
+ )
551
+
552
+
553
+ @app.get(
554
+ "/api/v1/sessions/{session_id}/sponsors",
555
+ response_model=list[VouchResponse],
556
+ tags=["Liability"],
557
+ )
558
+ async def list_vouches(session_id: str) -> list[VouchResponse]:
559
+ """List all sponsors in a session."""
560
+ _get_managed(session_id)
561
+ hv = _hv()
562
+ return [
563
+ VouchResponse(
564
+ vouch_id=v.vouch_id,
565
+ voucher_did=v.voucher_did,
566
+ vouchee_did=v.vouchee_did,
567
+ session_id=v.session_id,
568
+ bonded_amount=v.bonded_amount,
569
+ bonded_sigma_pct=v.bonded_sigma_pct,
570
+ is_active=v.is_active,
571
+ )
572
+ for v in hv.vouching._vouches.values()
573
+ if v.session_id == session_id
574
+ ]
575
+
576
+
577
+ @app.get(
578
+ "/api/v1/agents/{agent_did}/liability",
579
+ response_model=LiabilityExposureResponse,
580
+ tags=["Liability"],
581
+ )
582
+ async def get_agent_liability(agent_did: str) -> LiabilityExposureResponse:
583
+ """Get an agent's liability exposure across all sessions."""
584
+ hv = _hv()
585
+ vouches_given = []
586
+ vouches_received = []
587
+ total_exposure = 0.0
588
+
589
+ for v in hv.vouching._vouches.values():
590
+ vr = VouchResponse(
591
+ vouch_id=v.vouch_id,
592
+ voucher_did=v.voucher_did,
593
+ vouchee_did=v.vouchee_did,
594
+ session_id=v.session_id,
595
+ bonded_amount=v.bonded_amount,
596
+ bonded_sigma_pct=v.bonded_sigma_pct,
597
+ is_active=v.is_active,
598
+ )
599
+ if v.voucher_did == agent_did:
600
+ vouches_given.append(vr)
601
+ if v.is_active and not v.is_expired:
602
+ total_exposure += v.bonded_amount
603
+ if v.vouchee_did == agent_did:
604
+ vouches_received.append(vr)
605
+
606
+ return LiabilityExposureResponse(
607
+ agent_did=agent_did,
608
+ vouches_given=vouches_given,
609
+ vouches_received=vouches_received,
610
+ total_exposure=total_exposure,
611
+ )
612
+
613
+
614
+ # ── Events ──────────────────────────────────────────────────────────────────
615
+
616
+ @app.get("/api/v1/events", response_model=list[EventResponse], tags=["Events"])
617
+ async def query_events(
618
+ event_type: str | None = Query(None, description="Filter by event type"),
619
+ session_id: str | None = Query(None, description="Filter by session ID"),
620
+ agent_did: str | None = Query(None, description="Filter by agent DID"),
621
+ limit: int | None = Query(None, description="Max events to return"),
622
+ ) -> list[EventResponse]:
623
+ """Query events with optional filters."""
624
+ bus = _bus()
625
+ et = None
626
+ if event_type:
627
+ try:
628
+ et = EventType(event_type)
629
+ except ValueError:
630
+ raise HTTPException(status_code=400, detail=f"Unknown event type: {event_type}")
631
+ events = bus.query(event_type=et, session_id=session_id, agent_did=agent_did, limit=limit)
632
+ return [
633
+ EventResponse(
634
+ event_id=e.event_id,
635
+ event_type=e.event_type.value,
636
+ timestamp=e.timestamp.isoformat(),
637
+ session_id=e.session_id,
638
+ agent_did=e.agent_did,
639
+ causal_trace_id=e.causal_trace_id,
640
+ payload=e.payload,
641
+ )
642
+ for e in events
643
+ ]
644
+
645
+
646
+ @app.get(
647
+ "/api/v1/events/stats",
648
+ response_model=EventStatsResponse,
649
+ tags=["Events"],
650
+ )
651
+ async def get_event_stats() -> EventStatsResponse:
652
+ """Get event type counts."""
653
+ bus = _bus()
654
+ return EventStatsResponse(
655
+ total_events=bus.event_count,
656
+ by_type=bus.type_counts(),
657
+ )
658
+
659
+
660
+ # ── Audit endpoints ─────────────────────────────────────────────────────────
661
+
662
+ @app.get("/api/v1/audit/commitments", response_model=list[CommitmentResponse], tags=["Audit"])
663
+ async def list_commitments():
664
+ """List all session commitments."""
665
+ engine = _hv().commitment_engine
666
+ return [
667
+ CommitmentResponse(
668
+ session_id=r.session_id,
669
+ hash_chain_root=r.hash_chain_root,
670
+ participant_dids=r.participant_dids,
671
+ delta_count=r.delta_count,
672
+ committed_at=r.committed_at.isoformat(),
673
+ committed_to=r.committed_to,
674
+ blockchain_tx_id=r.blockchain_tx_id,
675
+ )
676
+ for r in engine._commitments.values()
677
+ ]
678
+
679
+ @app.get("/api/v1/audit/commitments/{session_id}", response_model=CommitmentResponse, tags=["Audit"])
680
+ async def get_commitment(session_id: str):
681
+ """Get commitment for a specific session."""
682
+ engine = _hv().commitment_engine
683
+ record = engine.get_commitment(session_id)
684
+ if not record:
685
+ raise HTTPException(status_code=404, detail="Commitment not found")
686
+ return CommitmentResponse(
687
+ session_id=record.session_id,
688
+ hash_chain_root=record.hash_chain_root,
689
+ participant_dids=record.participant_dids,
690
+ delta_count=record.delta_count,
691
+ committed_at=record.committed_at.isoformat(),
692
+ committed_to=record.committed_to,
693
+ blockchain_tx_id=record.blockchain_tx_id,
694
+ )
695
+
696
+ @app.post("/api/v1/audit/verify/{session_id}", response_model=VerifyCommitmentResponse, tags=["Audit"])
697
+ async def verify_commitment(session_id: str, expected_root: str = Query(...)):
698
+ """Verify a session's audit log root matches its commitment."""
699
+ engine = _hv().commitment_engine
700
+ record = engine.get_commitment(session_id)
701
+ if not record:
702
+ raise HTTPException(status_code=404, detail="Commitment not found")
703
+ valid = engine.verify(session_id, expected_root)
704
+ return VerifyCommitmentResponse(
705
+ session_id=session_id,
706
+ valid=valid,
707
+ committed_root=record.hash_chain_root,
708
+ expected_root=expected_root,
709
+ )
710
+
711
+ # ── Verification endpoints ──────────────────────────────────────────────────
712
+
713
+ @app.post("/api/v1/verify/history", response_model=VerifyHistoryResponse, tags=["Verification"])
714
+ async def verify_agent_history(request: VerifyHistoryRequest):
715
+ """Verify an agent's transaction history."""
716
+ from hypervisor.verification.history import TransactionRecord
717
+ records = [
718
+ TransactionRecord(
719
+ session_id=r.session_id,
720
+ summary_hash=r.summary_hash,
721
+ timestamp=r.timestamp,
722
+ participant_count=r.participant_count,
723
+ )
724
+ for r in request.transactions
725
+ ]
726
+ verifier = _hv().history_verifier
727
+ result = verifier.verify(request.agent_did, records)
728
+ return VerifyHistoryResponse(
729
+ agent_did=result.agent_did,
730
+ status=result.status.value,
731
+ transactions_checked=result.transactions_checked,
732
+ transactions_found=result.transactions_found,
733
+ inconsistencies=result.inconsistencies,
734
+ is_trustworthy=result.is_trustworthy,
735
+ cached=result.cached,
736
+ )
737
+
738
+ @app.delete("/api/v1/verify/cache/{agent_did}", tags=["Verification"])
739
+ async def clear_verification_cache(agent_did: str):
740
+ """Clear cached verification result for an agent."""
741
+ _hv().history_verifier.clear_cache(agent_did)
742
+ return {"status": "cleared", "agent_did": agent_did}