This repository has been archived on 2026-04-16. You can view files and clone it, but cannot push or open issues or pull requests.
llm-principal-broker/llm_broker/main.py
Tyler King 944b3fde19 feat: LLM Principal Broker MVP
FastAPI companion service to the GSAP broker for AI agent
identity delegation in governed shell sessions.

Implements GCAP-SPEC-LLM-PRINCIPAL-BROKER-0001:
  POST /delegate — request delegation (human → AI agent)
  POST /delegate/{id}/revoke — revoke delegation
  GET  /delegate/{id} — delegation status
  GET  /agents — list active delegations
  GET  /health — health check

Delegation lifecycle:
  REQUESTED → ACTIVE → EXPIRED | REVOKED
  Cascading revocation on delegator AC revocation
  Background cleanup of expired delegations (30s interval)

Keycloak integration:
  Registers ephemeral agent clients per delegation
  Deletes clients on revocation/expiry
  Dev mode: stubs when no client_secret configured

GSAP broker integration:
  Requests delegated ACs via on_behalf_of pattern
  Scope narrowing: agent ceiling ≤ delegator ceiling
  Dev mode: stubs when no bearer_token configured

Chronicle integration:
  DELEGATION_CREATED (0x3001)
  DELEGATION_REVOKED (0x3003)
  DELEGATION_EXPIRED (0x3004)

All 7 smoke tests pass (health, create, list, query, revoke, verify, empty).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:28:37 -04:00

146 lines
4.4 KiB
Python

"""LLM Principal Broker — GCAP-SPEC-LLM-PRINCIPAL-BROKER-0001.
AI agent identity delegation for governed shell sessions.
Companion service to the GSAP broker.
"""
import asyncio
import logging
from contextlib import asynccontextmanager
from datetime import datetime
import structlog
from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
from llm_broker import db
from llm_broker.delegation import DelegationManager
from llm_broker.models import (
ActiveDelegation,
AgentListResponse,
DelegationInfo,
DelegationRequest,
DelegationResponse,
DelegationStatus,
RevokeRequest,
RevokeResponse,
)
from llm_broker.settings import settings
logger = structlog.get_logger()
manager = DelegationManager(settings)
@asynccontextmanager
async def lifespan(app: FastAPI):
await db.init_db()
logger.info("llm-principal-broker started", broker_did=settings.broker_did)
task = asyncio.create_task(_cleanup_loop())
yield
task.cancel()
async def _cleanup_loop():
"""Periodically expire stale delegations (30s interval)."""
while True:
try:
count = await manager.cleanup_expired()
except Exception as e:
logger.warning("cleanup error", error=str(e))
await asyncio.sleep(30)
app = FastAPI(
title="LLM Principal Broker",
description="AI agent identity delegation — GCAP-SPEC-LLM-PRINCIPAL-BROKER-0001",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post("/delegate", response_model=DelegationResponse, tags=["Delegation"])
async def create_delegation(
request: DelegationRequest,
x_delegator_did: str = Header(..., alias="X-Delegator-DID"),
):
"""Request delegation of authority to an AI agent — §8.1."""
try:
return await manager.create_delegation(request, x_delegator_did)
except RuntimeError as e:
raise HTTPException(status_code=502, detail=str(e))
@app.post("/delegate/{delegation_id}/revoke", response_model=RevokeResponse, tags=["Delegation"])
async def revoke_delegation(
delegation_id: str,
request: RevokeRequest = RevokeRequest(),
):
"""Revoke an active delegation — §8.2."""
success = await manager.revoke_delegation(delegation_id, request.reason)
if not success:
raise HTTPException(status_code=404, detail="Delegation not found or not active")
return RevokeResponse(delegation_id=delegation_id, reason=request.reason)
@app.get("/delegate/{delegation_id}", response_model=DelegationInfo, tags=["Delegation"])
async def get_delegation(delegation_id: str):
"""Query delegation status — §8.3."""
d = await db.get_delegation(delegation_id)
if not d:
raise HTTPException(status_code=404, detail="Delegation not found")
now = datetime.utcnow()
ttl_remaining = max(0, int((d.expires_at - now).total_seconds()))
return DelegationInfo(
delegation_id=d.delegation_id,
status=DelegationStatus(d.status),
agent_did=d.agent_did,
agent_type=d.agent_type,
delegator_did=d.delegator_did,
commands_executed=d.commands_executed,
commands_remaining=max(0, d.max_commands - d.commands_executed),
ttl_remaining_seconds=ttl_remaining,
created_at=d.created_at.isoformat(),
expires_at=d.expires_at.isoformat(),
)
@app.get("/agents", response_model=AgentListResponse, tags=["Delegation"])
async def list_agents():
"""List all active agent delegations — §8.4."""
active = await db.get_active_delegations()
now = datetime.utcnow()
return AgentListResponse(
active_delegations=[
ActiveDelegation(
delegation_id=d.delegation_id,
agent_type=d.agent_type,
delegator=d.delegator_did,
commands_executed=d.commands_executed,
ttl_remaining_seconds=max(0, int((d.expires_at - now).total_seconds())),
status=d.status,
)
for d in active
],
total_active=len(active),
)
@app.get("/health", tags=["Health"])
async def health():
active = await db.get_active_delegations()
return {
"status": "healthy",
"service": "llm-principal-broker",
"version": "0.1.0",
"active_delegations": len(active),
}