Merges the standalone llm-principal-broker (1,132 LOC) into fastapi-gsap
as an in-process module. The previous architecture had two FastAPI
processes where the broker called GSAP over HTTP on every delegation
creation; now the lifecycle code uses GSAP's own async DB engine
directly and inserts AuthorizationContextDB rows in the same
transaction context.
New module: gsap_broker/delegations/
models.py Pydantic request/response shapes
storage.py DelegationDB SQLModel sharing the GSAP engine
lifecycle.py DelegationManager — in-process AC issuance via
AuthorizationContextDB.insert (no HTTP self-call)
cleanup.py 30s background task for stale delegations
router.py /delegations/* FastAPI router (4 endpoints)
registrars/
base.py AgentRegistrar Protocol + AgentCredentials
stub.py dev-mode no-op
keycloak.py Keycloak Admin REST API
entra.py Microsoft Entra Agent ID via Graph (lazy import)
factory.py driver selection (auto/stub/keycloak/entra)
Wiring:
app.py mounts the delegations router and starts the cleanup task in
the existing lifespan context manager.
settings.py absorbs the keycloak_admin_*, entra_*, and
agent_registrar fields from the old broker's settings.
pyproject.toml adds an optional `entra` extra for the msal dep.
Behaviour preservation:
- Endpoints kept identical: POST /, POST /{id}/revoke, GET /{id}, GET /
- Chronicle event codes preserved: 0x3001 / 0x3003 / 0x3004
- DelegationScope defaults unchanged (max_ttl_minutes=60, max_commands=500)
- Capability ceiling -> capability_mask conversion documented inline
Smoke test: `python -c "from gsap_broker.app import app"` loads cleanly
with 26 routes including the four /delegations/ endpoints.
The standalone llm-principal-broker repo is archived to
~/projects/archive/llm-principal-broker.
Signed-off-by: Tyler King <tking@guildhouse.dev>
96 lines
2.4 KiB
Python
96 lines
2.4 KiB
Python
"""Pydantic models for delegation lifecycle.
|
|
|
|
Originally from llm-principal-broker (GCAP-SPEC-LLM-PRINCIPAL-BROKER-0001
|
|
§3, §8). Absorbed into fastapi-gsap as the delegations submodule so that
|
|
the lifecycle bookkeeping shares process and database with the AC issuance
|
|
endpoints. The previous standalone service made an HTTP call back to GSAP
|
|
on every delegation creation; now it is an in-process function call.
|
|
"""
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional
|
|
from enum import Enum
|
|
|
|
|
|
class DelegationStatus(str, Enum):
|
|
REQUESTED = "requested"
|
|
ACTIVE = "active"
|
|
EXPIRED = "expired"
|
|
REVOKED = "revoked"
|
|
|
|
|
|
class DelegationScope(BaseModel):
|
|
inherit_corpus: bool = True
|
|
inherit_contexts: bool = True
|
|
capability_ceiling: str = "CAP_MUTATE"
|
|
ceremony_required_for: list[str] = Field(
|
|
default_factory=lambda: ["delete", "destroy", "drop"]
|
|
)
|
|
prohibited_commands: list[str] = Field(default_factory=list)
|
|
max_ttl_minutes: int = 60
|
|
max_commands: int = 500
|
|
|
|
|
|
class DelegationRequest(BaseModel):
|
|
"""POST /delegations/ request body — §8.1."""
|
|
delegator_ac_id: str
|
|
agent_type: str = "claude-code"
|
|
agent_model: Optional[str] = None
|
|
scope: Optional[DelegationScope] = None
|
|
accord_template: str = "ai-delegation-standard"
|
|
|
|
|
|
class AgentPrincipal(BaseModel):
|
|
did: str
|
|
keycloak_client_id: str
|
|
display_name: str
|
|
|
|
|
|
class DelegationResponse(BaseModel):
|
|
"""POST /delegations/ response — §3.2."""
|
|
delegation_id: str
|
|
agent_principal: AgentPrincipal
|
|
delegated_ac: dict
|
|
agent_token: str
|
|
expires_at: str
|
|
max_commands: int
|
|
chronicle_cid: Optional[str] = None
|
|
|
|
|
|
class DelegationInfo(BaseModel):
|
|
"""GET /delegations/{id} response — §8.3."""
|
|
delegation_id: str
|
|
status: DelegationStatus
|
|
agent_did: str
|
|
agent_type: str
|
|
delegator_did: str
|
|
commands_executed: int
|
|
commands_remaining: int
|
|
ttl_remaining_seconds: int
|
|
created_at: str
|
|
expires_at: str
|
|
|
|
|
|
class RevokeRequest(BaseModel):
|
|
reason: str = "manual_revocation"
|
|
|
|
|
|
class RevokeResponse(BaseModel):
|
|
delegation_id: str
|
|
status: str = "revoked"
|
|
reason: str
|
|
chronicle_cid: Optional[str] = None
|
|
|
|
|
|
class ActiveDelegation(BaseModel):
|
|
delegation_id: str
|
|
agent_type: str
|
|
delegator: str
|
|
commands_executed: int
|
|
ttl_remaining_seconds: int
|
|
status: str
|
|
|
|
|
|
class AgentListResponse(BaseModel):
|
|
active_delegations: list[ActiveDelegation]
|
|
total_active: int
|