API Reference
Every public method on the SDK. All async-first, all type-hinted.
SDK Entry Point
GovernanceSDK(database_url=..., audit_secret=..., cost_fail_open=False, hot_reload=False, hot_reload_interval=30, enable_audit=True, enable_scope=True, enable_cost=True, enable_gates=True, enable_loop=True, enable_presence=True, enable_prompts=True, enable_routing=False, verify_chain_on_read=False, warn_on_no_wrappers=True, default_max_tokens=None)Main entry point. Connects to Postgres and initializes the enabled enforcement modules. Use as an async context manager for automatic start/close. Every module is opt-in via an enable_* flag — when False, the module is not constructed and accessing sdk.<module> raises AttributeError. verify_chain_on_read (v0.5.3) verifies the HMAC chain on every get_events() call. warn_on_no_wrappers (v0.5.3) emits a structlog warning at start() when no wrap_openai / wrap_anthropic has been registered. default_max_tokens (v0.5.3) sets the ceiling used by the projected budget gate when callers do not declare max_tokens. v0.5.4 auto-wires PresenceModule into ScopeModule for kill-switch enforcement when both enable_scope=True and enable_presence=True (both defaults) — setting enable_presence=False silently disables the kill switch, so scope.check() will no longer read operator-written kill markers.
Returns: GovernanceSDK instance with the modules constructed for the enabled flags
Raises: ValueError if neither database_url nor api_key is provided, if the audit secret is too short / too weak, or if default_max_tokens is < 1
Note: v0.5.3 adds enable_loop, enable_presence, verify_chain_on_read, warn_on_no_wrappers, and default_max_tokens flags. v0.5.4 wires kill-switch enforcement through scope.check() when enable_presence and enable_scope are both true (both defaults). See the README Configuration Reference for complete option documentation.
# Full enforcement (all modules, wrapper-coverage warning, projected budget ceiling)
async with GovernanceSDK(
database_url=os.environ["DATABASE_URL"],
default_max_tokens=4096,
verify_chain_on_read=True,
) as sdk:
await sdk.audit.log(AuditEvent(agent_id="x", kind="y"))
# Lightweight — no loop detection, no presence, audit-only
async with GovernanceSDK(
database_url=os.environ["DATABASE_URL"],
enable_loop=False,
enable_presence=False,
warn_on_no_wrappers=False,
) as sdk:
... # sdk.loop / sdk.presence raise AttributeError on accessGovernanceSDKSync (Sync Wrapper)v0.5.0
GovernanceSDKSync(database_url=..., audit_secret=..., **kwargs)Synchronous wrapper around GovernanceSDK. Spins up a daemon thread with a dedicated asyncio event loop. All async module methods (audit.log, cost.track, scope.check, etc.) are exposed as blocking sync calls. Supports context-manager usage. The background thread is a daemon thread so it does not prevent interpreter shutdown.
Returns: GovernanceSDKSync instance with .audit, .scope, .cost, .gates, .loop, .presence, .contracts modules (all sync)
Raises: RuntimeError if the background event loop stops unexpectedly
from codeatelier_governance import GovernanceSDKSync, AuditEvent
with GovernanceSDKSync(database_url=os.environ["DATABASE_URL"]) as sdk:
sdk.audit.log(AuditEvent(agent_id="my-agent", kind="tool.call"))
sdk.scope.check("my-agent", tool="send_email")
sdk.cost.check_or_raise("my-agent", session_id)GovernanceSDKSync.start() / .close()Explicit lifecycle management. start() initializes the underlying async SDK (audit flusher, hot-reload, etc.). close() drains in-flight events and stops the background loop. Both are called automatically when using the context manager.
Top-level Exportsv0.5.0
These classes are now exported from the top-level codeatelier_governance package for convenience:
AuditEvent — previously only from codeatelier_governance.audit
ScopePolicy — previously only from codeatelier_governance.scope
BudgetPolicy — previously only from codeatelier_governance.cost
LoopPolicy, LoopDetected — from codeatelier_governance.loop
Contract, PreCondition, PostCondition, ContractViolation — from contracts
AgentStatus, GovernanceConfig, GovernanceSDKSync
AgentKilledError (v0.5.4) — raised by sdk.presence.assert_alive() and by sdk.scope.check() when an operator has killed the agent. Import via from codeatelier_governance.presence import AgentKilledError.
Error Hierarchyv0.5.0
GovernanceError — base class for all SDK exceptions. Every subclass carries a recovery_hint field with an actionable next step.
ScopeViolation — tool or API not in the whitelist. Error message now includes the allowed tools list for debugging.
BudgetExceeded — session or daily cap hit. Includes recovery_hint with the specific cap that was exceeded.
LoopDetected — agent exceeded max calls in sliding window.
ContractViolation — pre or post condition failed.
ChainIntegrityError — HMAC verification or fork detection failed.
PolicyNotRegistered — scope check on an agent with no policy.
AgentKilledError (v0.5.4, RuntimeError subclass — NOT under GovernanceError) — raised when an operator has killed the agent via the console halt button or a direct write to the kill marker. Important: this is a RuntimeError, not a GovernanceError subclass. A bare except GovernanceError or except ScopeViolation will NOT catch a kill. Catch explicitly with except AgentKilledError if you need to handle the kill in application code.
Audit Module
AuditEvent.model: str | Nonev0.2.1Optional model name (e.g. "gpt-4o-mini"). First-class field included in the HMAC computation, so changing it after the fact breaks the chain. Max 128 characters.
AuditEventRecord.is_placeholder: bool (property)v0.2.1Returns True when the record was synthesized because both primary and fallback storage were unreachable. A placeholder has hmac == "0" * 64 and metadata["audit.unavailable"] == True. Operators should alert on these.
await sdk.audit.log(event: AuditEvent)Log an audit event. Computes the HMAC chain atomically with the insert (advisory lock per session). Non-breaking: never raises on internal failure — returns a placeholder record instead.
Returns: AuditEventRecord (check record.is_placeholder to detect degraded state)
Note: Observation surface — never breaks the host call.
from codeatelier_governance.audit import AuditEvent
record = await sdk.audit.log(AuditEvent(
agent_id="billing-agent",
kind="llm.call",
model="gpt-4o-mini",
metadata={"prompt_tokens": 150},
))@sdk.audit.track(kind="...", agent_id="...")Decorator that logs entry/exit events for an async function. Computes input_hash from args and output_hash from the return value. On exception, logs a .error event and re-raises.
Returns: The wrapped function's return value (unchanged)
Note: Async functions only. Observation surface.
@sdk.audit.track(kind="charge_card", agent_id="billing-agent")
async def charge(amount: int) -> str:
return await stripe.charge(amount)await sdk.audit.trace_session_chain(session_id: UUID)Walk the HMAC chain for an entire session. Re-verifies every row's HMAC against the secret. The compliance officer's one-click verification.
Returns: list[AuditEventRecord] in chain order
Raises: ChainIntegrityError at the first tampered row
chain = await sdk.audit.trace_session_chain(session_id)
for event in chain:
print(f"{event.kind} — {event.agent_id} — hmac verified")await sdk.audit.verify_chain(session_id: UUID | None = None, from_seq: int | None = None, to_seq: int | None = None)On-demand HMAC chain verification. Stable public API for compliance attestation and post-incident review. Supports partial-range verification via from_seq and to_seq — useful for verifying only the window relevant to a specific incident without paying the O(n) cost on the whole chain. Two-pass verification: recomputes each row's HMAC and checks prev_hash linkage (fork detection). Never returns False — clean chain returns True, any failure raises.
Returns: True on a clean chain
Raises: ChainIntegrityError with the offending sequence number on the first broken HMAC or prev_hash linkage
Note: Added in v0.5.3. Set verify_chain_on_read=True on GovernanceSDK to verify the chain automatically on every get_events() call.
# Full-chain verification for a compliance attestation run
await sdk.audit.verify_chain()
# Window verification — only events [100..200] in one session
await sdk.audit.verify_chain(session_id=sid, from_seq=100, to_seq=200)
# Automatic on-read verification
sdk = GovernanceSDK(database_url=..., verify_chain_on_read=True)
events = await sdk.audit.get_events(session_id=sid) # raises on tampersdk.audit.subscribe(callback: Callable[[AuditEventRecord], Awaitable[None]])v0.2.1Register a callback invoked after every successful log. Subscribers run after the record is enqueued for write — a failing subscriber never breaks the audit chain or the host application. Used by the OTel exporter and any custom consumer that wants to fan audit events out to a secondary system.
Returns: None
Note: Exceptions raised by subscribers are caught, logged, and swallowed.
Scope Module
sdk.scope.register(policy: ScopePolicy)Register a scope policy for an agent. Call at app startup. Policies are also persisted to Postgres (best-effort) so the console GUI can display them.
from codeatelier_governance.scope import ScopePolicy
sdk.scope.register(ScopePolicy(
agent_id="billing-agent",
allowed_tools=frozenset({"read_invoice", "send_email"}),
allowed_apis=frozenset({"GET https://api.stripe.com/v1/*"}),
hidden_tools=frozenset({"delete_all", "admin_reset"}),
))await sdk.scope.check(agent_id, tool=..., api=...)Check if the agent is allowed to call the given tool or API. Default-deny: an agent with no registered policy fails every check.
Raises: ScopeViolation (auto audit-logged) or PolicyNotRegistered
Note: Enforcement surface — raises by contract.
await sdk.scope.check(agent_id="billing-agent", tool="read_invoice") # passes
await sdk.scope.check(agent_id="billing-agent", tool="delete_user") # raises ScopeViolationsdk.scope.filter_tools(agent_id: str, tools: list[str]) -> list[str]v0.2.2Remove hidden tools from a tool list before passing it to the LLM. The agent never sees the hidden tools in its context — they are removed entirely, not just blocked at call time. If no policy is registered for the agent, the full list is returned unchanged.
Returns: Filtered list[str] with hidden tools removed
Note: Synchronous method — no await needed.
ScopePolicy.hidden_tools: frozenset[str]v0.2.2Set of tool names to remove from the LLM's context entirely. Unlike allowed_tools (which blocks calls at execution time), hidden tools are filtered out before the LLM sees the tool list — the agent cannot even attempt to call them.
Cost Module
sdk.cost.register(policy: BudgetPolicy)Register a budget policy for an agent. Caps: per_session_usd, per_session_tokens, per_agent_usd_daily, per_agent_tokens_daily, per_session_seconds. At least one cap required.
from codeatelier_governance.cost import BudgetPolicy
sdk.cost.register(BudgetPolicy(
agent_id="billing-agent",
per_session_usd=0.50,
per_session_seconds=300, # 5-minute time limit
per_agent_usd_daily=10.00,
))await sdk.cost.check_or_raise(agent_id, session_id)Pre-call enforcement: raises BudgetExceeded if any cap is hit, including session time limits. Fail-closed by default — if the cost store is unreachable, the call is denied.
Raises: BudgetExceeded (auto audit-logged)
Note: Enforcement surface — raises by contract. Pass cost_fail_open=True at SDK init for availability over safety.
await sdk.cost.check_or_raise("billing-agent", session_id)
# If this line runs, the budget is not exceeded — proceed with the LLM callawait sdk.cost.track_usage(agent_id, session_id, *, model, input_tokens, output_tokens)v0.2.2Post-call: record actual usage with automatic USD computation from model name. Built-in pricing covers 24 models from OpenAI, Anthropic, Google, Meta, and Mistral. Prefix matching handles versioned names (e.g. gpt-4o-2024-05-13 matches gpt-4o). Unknown models are zero-costed (non-breaking).
Returns: None
Note: Observation surface — never raises. Convenience wrapper around track() that auto-computes usd.
await sdk.cost.track(agent_id, session_id, tokens=..., usd=...)Post-call: record actual usage with explicit token count and USD. Counters are monotonic (negative deltas rejected). Multi-process correct via Postgres UPSERT. Use track_usage() instead if you want auto USD computation.
Note: Observation surface — never raises.
await sdk.cost.track("billing-agent", session_id, tokens=150, usd=0.003)BudgetPolicy.per_session_seconds: int | Nonev0.2.2Maximum session duration in seconds (0-86400, max 24 hours). Enforced by check_or_raise() — raises BudgetExceeded when the session has been running longer than the limit. Session start time is tracked automatically in Postgres.
await sdk.cost.model_breakdown(agent_id: str)v0.3.0Returns per-model cost aggregation for an agent. Each key is a model name with USD and token totals. Useful for understanding which models drive the most spend.
Returns: dict[str, {"usd": float, "tokens": int}] — e.g. {"gpt-4o": {"usd": 0.0075, "tokens": 1500}}
Note: Observation surface — never raises. Returns empty dict if no usage recorded.
await sdk.cost.snapshot(agent_id: str, session_id: UUID)v0.2.1Read-only snapshot of current usage and remaining budget for an agent/session pair. Returns a BudgetSnapshot with fields for session and daily usage (USD + tokens) and the remaining budget under each registered cap.
Returns: BudgetSnapshot (session_usd_used, session_tokens_used, agent_daily_usd_used, agent_daily_tokens_used, plus *_remaining counterparts)
Note: Defensive — if storage fails, returns zeros and logs the error. Never raises.
Loop Detection Modulev0.3.0
sdk.loop.register(policy: LoopPolicy)Register a loop detection policy for an agent. Defines the sliding window size, maximum calls allowed, and action to take on detection.
from codeatelier_governance.loop import LoopPolicy
sdk.loop.register(LoopPolicy(
agent_id="my-agent",
window_seconds=60,
max_calls=5,
action="raise", # or "log"
))await sdk.loop.record_call(agent_id: str, session_id: UUID, tool_name: str)Record a tool call and check for loops. If the same tool has been called more than max_calls times within the sliding window for this session, the configured action fires. Tool names are normalized to lowercase for case-insensitive detection.
Raises: LoopDetected (when action="raise" and threshold exceeded)
Note: Enforcement surface when action="raise". Emits a loop.detected audit event on detection.
from codeatelier_governance.loop import LoopDetected
try:
await sdk.loop.record_call("my-agent", session_id, "search_web")
except LoopDetected:
# Agent called search_web 5+ times in 60s — break the loop
breakawait sdk.loop.check(agent_id: str, session_id: UUID)Read-only check: returns whether the agent is currently in a detected loop state for the session. Does not record a new call.
Returns: bool — True if a loop is currently detected
Note: Observation surface — never raises.
if await sdk.loop.check("my-agent", session_id):
# Loop detected — take corrective action
await escalate_to_human(session_id)Presence Modulev0.3.0
await sdk.presence.heartbeat(agent_id: str, metadata: dict | None = None, operator_id: str | None = None)Mark an agent as live. Call periodically (e.g. every 30s) to maintain the "Live" status. Each heartbeat updates the last_seen timestamp in Postgres. Pass operator_id to bind the agent to a human operator — used by the console for self-approval prevention on HITL gates. Pass metadata for arbitrary JSON (max 64KB).
Note: Observation surface — never raises.
# In your agent's main loop:
await sdk.presence.heartbeat(
"billing-agent",
operator_id="alice@company.com",
metadata={"version": "1.2.0"},
)await sdk.presence.mark_idle(agent_id: str)Explicitly transition an agent to "Idle" status. Use when the agent is waiting for work but still running.
Note: Observation surface — never raises.
# Agent finished processing, waiting for next task:
await sdk.presence.mark_idle("billing-agent")await sdk.presence.close_agent(agent_id: str)Remove an agent from the presence table. Call during graceful shutdown. The agent will no longer appear in list_agents().
Note: Observation surface — never raises.
# Graceful shutdown:
await sdk.presence.close_agent("billing-agent")await sdk.presence.list_agents()Return all agents with their current status (Live, Idle, or Unresponsive) and last_seen timestamp.
Returns: list[AgentPresence] with .agent_id, .status, .last_seen
Note: Observation surface — never raises.
agents = await sdk.presence.list_agents()
for agent in agents:
print(f"{agent.agent_id}: {agent.status} (last seen {agent.last_seen})")await sdk.presence.check_stale(timeout_seconds: int = 300)Mark agents as "Unresponsive" if their last heartbeat is older than timeout_seconds. Call periodically from a health-check endpoint or cron job.
Returns: list[str] — agent IDs that were marked unresponsive
Note: Observation surface — never raises.
# Mark agents unresponsive if no heartbeat in 5 minutes:
stale = await sdk.presence.check_stale(timeout_seconds=300)
if stale:
alert(f"Unresponsive agents: {stale}")await sdk.presence.is_killed(agent_id: str)Return True if an operator has written a kill marker for this agent (via the console halt button or a direct write to the presence row). Reads from a 5-second TTL in-memory cache backed by governance_agent_presence.metadata_json — the hot path is one dict lookup, and the worst-case delay between an operator clicking halt and enforcement is bounded by the TTL. If the governance database is unreachable, the cache holds the last known state and a structlog warning is logged instead of raising. This is OBSERVATION ONLY — it does not raise or block anything. For actual enforcement, call assert_alive() instead (or let scope.check() call it for you, which it does automatically in v0.5.4).
Returns: bool
Note: Added in v0.5.4. Does not raise on DB outage — preserves CLAUDE.md Invariant #1 (host app keeps running when governance DB is unreachable). Observation-only: use assert_alive() for enforcement.
# Observation only — use assert_alive() for actual enforcement.
# This pattern is correct when you want to HANDLE the kill explicitly
# (e.g. flush state, notify upstream) before aborting.
if await sdk.presence.is_killed("billing-agent"):
await flush_partial_results()
await notify_upstream("halted by operator")
raise RuntimeError("billing-agent killed — aborting task")
# For enforcement, prefer assert_alive():
await sdk.presence.assert_alive("billing-agent") # raises AgentKilledError on killawait sdk.presence.assert_alive(agent_id: str)Raise AgentKilledError if the agent has been killed, with the operator, timestamp, and reason from the kill marker. This is the fail-closed gate called from sdk.scope.check() as of v0.5.4. Can also be called directly if you want to gate your own enforcement path against the kill switch.
Returns: None (raises AgentKilledError on kill)
Note: Added in v0.5.4. Wired automatically at the top of scope.check() when enable_presence=True. Cost gates, HITL gates, and LLM wrappers get the same wiring in v0.6.
from codeatelier_governance.presence import AgentKilledError
try:
await sdk.presence.assert_alive("billing-agent")
except AgentKilledError as e:
logger.error(
"agent killed",
agent_id=e.agent_id,
killed_by=e.killed_by,
killed_at=e.killed_at,
reason=e.reason,
)
raiseawait sdk.presence.force_refresh_killed_cache()Bypass the 5-second TTL and refresh the killed-agent cache immediately from the database. Used by tests and by push-based invalidation paths in v0.6 (Postgres LISTEN/NOTIFY).
Returns: None
Note: Added in v0.5.4. You should rarely need this in application code — the TTL cache is designed to be transparent. Primary use is tests.
# In a test after simulating a console halt:
await sdk.presence.force_refresh_killed_cache()
assert await sdk.presence.is_killed("test-agent")Contracts Modulev0.4.0
Contract(agent_id: str, tool: str, pre: list[PreCondition], post: list[PostCondition])Pydantic model binding pre/post conditions to an (agent_id, tool) pair. Frozen after construction - the agent cannot mutate its own contract at runtime. Maximum 20 pre-conditions and 20 post-conditions per contract.
PreCondition(check: str, message: str, params: dict = {}) / PostCondition(check: str, message: str, params: dict = {}) Declarative condition models. Built-in pre-condition checks: hitl_approved, budget_available, scope_allowed, custom. Built-in post-condition checks: audit_logged, custom. Unknown checks fail closed.
sdk.contracts.register(contract: Contract)Register a behavioral contract for an (agent_id, tool) pair. Call at app startup. One contract per (agent_id, tool) pair - registering again overwrites the previous contract.
from codeatelier_governance.contracts.models import Contract, PreCondition, PostCondition
sdk.contracts.register(Contract(
agent_id="billing-agent",
tool="charge_customer",
pre=[
PreCondition(check="hitl_approved", message="Charges require human approval"),
PreCondition(check="budget_available", message="Budget must not be exceeded"),
],
post=[
PostCondition(check="audit_logged", message="Charge must be audit-logged"),
],
))async with sdk.contracts.enforce(agent_id: str, session_id: UUID, tool: str)Async context manager that runs check_pre before the body and check_post after. Unifies scope, cost, HITL, and custom checks into one declarative surface. If any pre-condition fails, raises ContractViolation before your code runs. If any post-condition fails, raises ContractViolation after.
Raises: ContractViolation (auto audit-logged as contract.pre_violation or contract.post_violation)
Note: Enforcement surface - raises by contract. No-op if no contract registered for this (agent_id, tool).
async with sdk.contracts.enforce("billing-agent", session_id, "charge_customer"):
# Pre-conditions verified - safe to proceed
result = await stripe.charge(amount)
# Post-conditions checked on exitawait sdk.contracts.check_pre(agent_id: str, session_id: UUID, tool: str)Evaluate all pre-conditions for this (agent_id, tool) pair. If any pre-condition fails, emits a contract.pre_violation audit event and raises. No-op if no contract is registered.
Raises: ContractViolation
Note: Enforcement surface - raises by contract.
await sdk.contracts.check_pre("billing-agent", session_id, "charge_customer")
# All pre-conditions passed - safe to proceedawait sdk.contracts.check_post(agent_id: str, session_id: UUID, tool: str)Evaluate all post-conditions for this (agent_id, tool) pair. If any post-condition fails, emits a contract.post_violation audit event and raises. No-op if no contract is registered.
Raises: ContractViolation
Note: Enforcement surface - raises by contract.
await sdk.contracts.check_post("billing-agent", session_id, "charge_customer")
# All post-conditions verifiedsdk.contracts.register_check(name: str, fn: Callable[[str, UUID, str], Awaitable[bool]])Register a custom async callable for use in custom pre/post conditions. The callable receives (agent_id, session_id, tool) and must return True (pass) or False (fail). Name must be 1-256 characters.
Returns: None
Raises: ValueError if name is empty or exceeds 256 characters
Compliance Modulev0.4.0
ComplianceReport(report_id, generated_at, format, agent_id, session_ids, date_range, sections, chain_integrity_status, coverage_caveat, coverage_pct)v0.5.3Top-level compliance report model. Immutable once generated. Serializable to JSON for archival or internal compliance review. The format field indicates the report type (e.g. "article12" or "summary").
v0.5.3 fields: chain_integrity_status is "verified", "failed", or "unverified" (default when no chain verification was requested). coverage_caveat is a required field on the ComplianceReport model and ReportGenerator always populates it with the standard scoping language: "This report covers only actions routed through the SDK wrappers. Actions made via direct LLM client calls are not included." (Callers constructing ComplianceReport directly must supply it — the field has no default on the Pydantic model.) coverage_pct is a Pydantic-validated [0.0, 1.0] float or None; reserved for the v0.6 wrapper registry and always None in v0.5.x. The field is always present — its absence would imply 100% coverage.
ReportSection(title: str, description: str, data: list[dict], status: "compliant" | "partial" | "non_compliant")A single section of a compliance report, mapping to a specific EU AI Act Article 12 requirement. The status field indicates whether the requirement is met based on available audit data.
ReportGenerator(database_url: str | None = None, *, audit_store: AuditStore | None = None, audit_module: AuditModule | None = None)Constructor for the compliance report generator. Provide database_url for Postgres queries or audit_store for in-memory/test usage. Pass audit_module=sdk.audit (v0.5.3+) to enable chain verification inside reports — required when calling generate_article12(verify_chain=True) or generate_summary(verify_chain=True).
Note: audit_module parameter added in v0.5.3.
from codeatelier_governance.compliance.report import ReportGenerator
# For CLI / archival reports
generator = ReportGenerator(database_url=os.environ["DATABASE_URL"])
# For chain-verified reports — wire the audit module
generator = ReportGenerator(
database_url=os.environ["DATABASE_URL"],
audit_module=sdk.audit,
)await generator.generate_article12(session_ids=None, agent_id=None, date_from=None, date_to=None, *, verify_chain=False)Generate an EU AI Act Article 12 evidence report. Queries audit events with the given filters and maps them to the seven Article 12 automatic logging requirements: event registration, duration of use, reference database, input data, functioning, human oversight, and post-market monitoring. The report provides evidence for actions the SDK observed; it does not assert compliance for the overall deployment.
Returns: ComplianceReport with seven ReportSection entries, each with a compliant/partial/non_compliant status, plus coverage_caveat, chain_integrity_status, and coverage_pct fields
Raises: ValueError if verify_chain=True is requested but audit_module was not passed to the ReportGenerator constructor — no silent degradation. IMPORTANT: you must construct ReportGenerator with audit_module=sdk.audit before calling this with verify_chain=True. See the ReportGenerator constructor entry above.
Note: verify_chain keyword-only parameter added in v0.5.3. Available via both the CLI (governance compliance report) and direct import.
from codeatelier_governance.compliance.report import ReportGenerator
from datetime import datetime, timezone
# Chain-verified Article 12 attestation
generator = ReportGenerator(
database_url=os.environ["DATABASE_URL"],
audit_module=sdk.audit,
)
report = await generator.generate_article12(
agent_id="billing-agent",
date_from=datetime(2026, 1, 1, tzinfo=timezone.utc),
date_to=datetime(2026, 4, 1, tzinfo=timezone.utc),
verify_chain=True, # runs HMAC chain verification
)
assert report.chain_integrity_status == "verified"
print(report.coverage_caveat)
for section in report.sections:
print(f"{section.title}: {section.status}")await generator.generate_summary(agent_id: str, date_from=None, date_to=None, *, verify_chain=False)Generate a high-level summary compliance report for an agent. Includes three sections: agent overview (event counts, models, sessions), violation summary (scope/budget/loop violations), and human oversight (HITL gate activity).
Returns: ComplianceReport with three ReportSection entries
Raises: ValueError if verify_chain=True is requested but audit_module was not passed to the ReportGenerator constructor
Note: verify_chain keyword-only parameter added in v0.5.3.
report = await generator.generate_summary(
agent_id="billing-agent",
date_from=datetime(2026, 3, 1, tzinfo=timezone.utc),
verify_chain=True,
)
for section in report.sections:
print(f"{section.title}: {section.status}")Anthropic Adapterv0.4.0
wrap_anthropic(client, sdk: GovernanceSDK, agent_id: str, session_id: UUID | None = None)Patch an Anthropic client to emit governance audit events and track cost automatically. Supports both anthropic.Anthropic (sync) and anthropic.AsyncAnthropic (async). The client is monkey-patched in-place and returned so existing references continue to work. Each messages.create() call will: (1) run cost.check_or_raise before the LLM call, (2) audit-log the call, (3) extract token usage from the response, (4) compute USD cost via the built-in pricing table, and (5) track cost post-call.
Returns: The same client object, patched in-place
Raises: BudgetExceeded (enforcement surface - checked before every LLM call)
Note: Audit logging and cost tracking are observation surfaces and never raise. The budget pre-check is the only enforcement surface. Calling wrap_anthropic twice on the same client is a no-op (logs a warning).
import anthropic
from codeatelier_governance import GovernanceSDK
from codeatelier_governance.integrations.anthropic_wrap import wrap_anthropic
async with GovernanceSDK(database_url=os.environ["DATABASE_URL"]) as sdk:
client = wrap_anthropic(anthropic.Anthropic(), sdk=sdk, agent_id="my-agent")
# client.messages.create() now auto-audits + tracks cost
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)Gates Module
await sdk.gates.request(kind, agent_id, payload=...)Open an approval request. Returns a pending request with a signed token. Surface the request to a human (Slack, email, console GUI). The human calls grant(token) or deny(token).
Returns: ApprovalRequest with .request_id, .token, .expires_at
Note: Observation surface — never raises on internal failure.
req = await sdk.gates.request(
kind="deploy.production",
agent_id="deploy-agent",
payload={"pr": 142, "environment": "production"},
)
print(f"Approval needed: {req.request_id}")await sdk.gates.grant(token) / deny(token)Resolve an approval request. Single-use: the same token cannot be used twice. Time-bound: expired tokens are rejected. Action-hash-bound: swapping the action invalidates the token.
Raises: ApprovalTokenError on forge, replay, expiry, or mismatch
await sdk.gates.grant(req.token) # approval.granted audit row writtenawait sdk.gates.wait_for(request_id, timeout=...)Block until the request is resolved. Polls the store every 500ms (with jitter). Multi-process correct: the grant can come from any worker.
Raises: ApprovalTimeout if nobody responds, ApprovalDenied if denied
Note: Enforcement surface — raises by contract.
granted = await sdk.gates.wait_for(req.request_id, timeout=600)
# If this line runs, a human approved the action@sdk.gates.require_approval(kind=..., agent_id=..., timeout=600, blocking=True)v0.2.1Decorator that opens an approval request before running the wrapped async function. If blocking=True (default), the call blocks until a human grants or denies. If blocking=False, raises ApprovalPending immediately and the caller resumes after resolution.
Returns:The wrapped function's return value (after approval)
Raises: ApprovalTimeout, ApprovalDenied, or ApprovalPending (non-blocking mode)