Specora Platform SDK for Python - AI governance and policy enforcement
Project description
specora
Specora Platform SDK for Python - AI governance and policy enforcement.
Installation
pip install specora
# or
poetry add specora
# or
uv add specora
Quick Start
If you already call OpenAI or Anthropic, governing those calls is a one-line swap. Replace your provider client with the matching Specora drop-in. Every call then goes through policy and budget enforcement, and each execution is recorded for the audit trail. You keep the same call sites.
from specora import GovernedOpenAI
client = GovernedOpenAI(
specora_api_key="sk_...", # from the dashboard: Settings -> SDK Keys
openai_api_key="sk-...", # your existing OpenAI key (or set OPENAI_API_KEY)
)
# Same call you already write — now governed.
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Summarize this PR."}],
)
print(response.content)
If a policy blocks the call or the budget is exhausted, the call raises before it
ever reaches the model (fail-closed), so no ungoverned request gets through.
GovernedAnthropic works the same way with client.messages.create(...), and
GovernedClient("openai", ...) / GovernedClient("anthropic", ...) is a factory
if you pick the provider at runtime.
With key_management_mode="specora_managed", Specora holds the provider
credential and proxies the call, so you do not pass a provider key at all.
Full control: the explicit flow
When you need to drive each governance step yourself, use SpecoraClient
directly. The drop-in clients above are a thin wrapper over exactly this.
from specora import SpecoraClient
client = SpecoraClient(
base_url="https://api.specora.ai",
api_key="sk_...",
vendor_id="vendor-uuid",
tenant_id="tenant-uuid",
)
# Check policy before AI execution
policy = client.check_policy(
subject={"type": "pr_run", "id": "run-123"},
)
if policy.decision == "block":
print(f"Blocked by policy: {policy.severity}")
exit(1)
# Execute your AI operation...
# Record the execution
result = client.record_execution({
"provider": "openai",
"model": "gpt-4",
"cost_micros": 5000, # $0.005
"input_tokens": 1000,
"output_tokens": 500,
"success": True,
})
Model A vs Model B routing
Most apps are Model A (org-scoped): the org is resolved from your API key, so
you omit vendor_id / tenant_id. This is the default for normal SaaS orgs and
matches @specora/sdk.
# Model A — org-scoped (recommended for most integrations)
client = SpecoraClient(
base_url="https://api.specora.ai",
api_key="sk_...",
key_management_mode="specora_managed",
)
Model B (vendor-reseller) is for nested vendor/tenant hierarchies. Supply both
vendor_id and tenant_id. Supplying exactly one raises ValueError — partial
configuration is almost always a bug.
# Model B — vendor/tenant-scoped
client = SpecoraClient(
base_url="https://api.specora.ai",
api_key="sk_...",
vendor_id="vendor-uuid",
tenant_id="tenant-uuid",
)
Server-side proxy (no provider keys in your app)
With key_management_mode="specora_managed", Specora holds provider credentials in
its KMS and injects them server-side. Your app holds no provider API keys.
# Chat-shaped call (provider/model routed + governed + journaled server-side)
resp = client.proxy_execute({
"provider": "anthropic",
"model": "claude-3-sonnet",
"messages": [{"role": "user", "content": "Summarize this PR"}],
"tools": [...], # optional tool-calling
"routing_hints": {"purpose": "summary", "agent_name": "pr-summarizer"},
})
print(resp.content, resp.tool_calls, resp.finish_reason)
For providers whose request/response shape does not fit the chat contract
(image, 3D, audio, video, segmentation), use the raw proxy. Report
cost_micros yourself — Specora cannot parse cost from arbitrary provider
bodies.
raw = client.proxy_execute_raw({
"provider": "fal",
"model": "fal-ai/fashn/v1.5",
"endpoint": "/fal-ai/fashn/v1.5",
"body": {"prompt": "a red dress"},
"cost_micros": 5000,
})
print(raw.status, raw.raw_response)
# Long-running media jobs: submit async, then poll a short request at a time.
job = client.proxy_execute_raw({
"provider": "fal",
"model": "fal-ai/trellis-2",
"endpoint": "/fal-ai/trellis-2",
"execution_mode": "async",
})
while True:
state = client.get_raw_proxy_job(job.job_id)
if state.status in ("completed", "failed"):
break
recording_failed is surfaced as resp.recording_failed_warning, never raised:
the execution succeeded and cost was incurred, so retrying would double-bill
(INV-SDK-PROXY-001).
Async Client
from specora import AsyncSpecoraClient
async with AsyncSpecoraClient(
base_url="https://api.specora.ai",
api_key="sk_...",
vendor_id="vendor-uuid",
tenant_id="tenant-uuid",
) as client:
policy = await client.check_policy(
subject={"type": "pr_run", "id": "run-123"},
)
Features
- Policy Evaluation: Check governance policies before AI operations
- Budget Management: Track and enforce spending limits
- Execution Recording: Record AI executions for tracking
- Assumptions Validation: Validate change assumptions
- Evidence Generation: Generate audit evidence bundles
- Risk Scoring: Get real-time risk metrics
- Webhook Verification: HMAC-SHA256 signature verification
- Async Support: Full async/await support with
AsyncSpecoraClient
Purpose-Based Routing
Assign specific LLM providers/models to specific agents or task types. For
example, route safety-rewriter to a local Ollama instance while routing
style-assistant to Claude Sonnet.
auth = client.pre_authorize(
provider="anthropic",
model="claude-sonnet-4-20250514",
estimated_tokens=2000,
estimated_cost_micros=500,
routing_hints={
"purpose": "safety-rewriter",
"agent_name": "gtm-920-content-generator",
},
)
# The server may override provider/model based on purpose routing policies
print(auth.selected_provider) # e.g., "ollama"
print(auth.selected_model) # e.g., "llama3:70b"
The org admin creates PURPOSE_ROUTING governance policies that map agents
and task types to target providers/models. When pre_authorize() includes
routing hints, the server applies matching purpose routing rules. If no
fallback is configured and the target model is unavailable, the request is
blocked (fail-closed).
Configuration
client = SpecoraClient(
# Required
base_url="https://api.specora.ai",
api_key="sk_...",
vendor_id="vendor-uuid",
tenant_id="tenant-uuid",
# Optional
fail_open=False, # Default: False (fail-closed)
timeout=30.0, # Request timeout in seconds
retries=2, # Retry attempts
)
Fail-Closed Semantics
By default, the SDK operates in fail-closed mode. This means:
- Network errors result in blocked decisions
- Server errors result in blocked decisions
- Timeouts result in blocked decisions
This ensures that if the Specora API is unavailable, your AI operations are blocked rather than allowed without governance.
To change this behavior:
client = SpecoraClient(
# ...
fail_open=True, # Errors result in allowed decisions
)
API Reference
check_policy(subject, inputs=None)
Evaluate governance policy for an operation.
result = client.check_policy(
subject={"type": "pr_run", "id": "run-123"},
inputs={"context": "value"}, # optional
)
# result.decision: "allow" | "block" | "warn"
# result.severity: "low" | "medium" | "high" | "critical"
# result.invariants: list of InvariantResult
# result.override: OverrideEligibility
validate_budget(dimension="cost")
Check budget status for the tenant.
budget = client.validate_budget()
# budget.budget_remaining: float (USD)
# budget.budget_total: float (USD)
# budget.usage_percent: float (0-100)
# budget.forecast_breach: bool
# budget.enforcement_state: "normal" | "throttled" | "blocked"
record_execution(request)
Record an AI execution for budget tracking.
result = client.record_execution({
"provider": "openai",
"model": "gpt-4",
"cost_micros": 5000, # 1/1,000,000 USD
"input_tokens": 1000,
"output_tokens": 500,
"duration_ms": 1500,
"success": True,
"external_run_id": "your-id", # optional
"metadata": {"key": "value"}, # optional
})
# result.execution_id: str
# result.budget_remaining_micros: int
# result.usage_percent: float
# result.enforcement_state: str
validate_assumptions(request)
Validate assumptions for a change.
from specora import Assumption
result = client.validate_assumptions({
"run_id": "run-123",
"assumptions": [
{"id": "a1", "description": "No breaking changes", "impact_level": "high"},
],
})
# result.severity: "low" | "medium" | "high" | "critical"
# result.verification_required: bool
# result.merge_eligible: bool
# result.violations: list of AssumptionsViolation
# result.recommendations: list of str
generate_evidence_bundle(request)
Generate a signed evidence bundle.
evidence = client.generate_evidence_bundle({
"run_id": "run-123",
"artifact_types": ["confidence_report", "trace"],
"include_provenance": True,
})
# evidence.bundle_id: str
# evidence.bundle_hash: str (sha256:...)
# evidence.storage_path: str
get_risk_score()
Get risk metrics for the tenant.
risk = client.get_risk_score()
# risk.risk_percentile: float (0-100)
# risk.drift_risk: float
# risk.override_frequency_risk: float
# risk.anomaly_status: "normal" | "elevated" | "critical"
verify_proof(manifest_hash) (PR-ENT-520-02)
Verify a proof by manifest hash.
result = client.verify_proof("abc123def456...")
# result.verified: bool
# result.proof_found: bool
# result.org_match: bool
# result.manifest_hash: str
# result.proof_type: str | None
# result.created_at: datetime | None
# result.published_root_match: bool | None
# result.manifest_spec_id: str | None # e.g., "proof-manifest"
# result.manifest_schema_version: str | None # e.g., "1.0.0"
Manifest Versioning (PR-ENT-520-02-PATCH-01): The response includes manifest_spec_id and manifest_schema_version for external verification. Store these values alongside the hash to enable offline verification with the correct contract version.
compute_proof_hash(data) (Offline Verification)
Compute a proof hash locally using canonical JSON serialization.
from specora import compute_proof_hash
# Compute hash matching server-side canonical JSON
hash_result = compute_proof_hash({
"id": "...",
"org_id": "...",
"root_type": "ledger_daily",
"root_hash": "...",
"leaf_count": 100,
"period_start": "2024-01-01T00:00:00+00:00",
"period_end": "2024-01-31T23:59:59+00:00",
"created_at": "2024-02-01T00:00:00+00:00",
})
print(f"Hash: {hash_result}") # SHA-256 hex
Canonical JSON Rules:
- Sorted keys (alphabetically)
- Compact separators (
","and":") - ASCII-safe encoding (
ensure_ascii=True) - UTF-8 encoding for bytes
Webhook Verification
Verify incoming webhooks from Specora:
from flask import Flask, request, jsonify
from specora import verify_webhook_signature, parse_webhook_payload
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
is_valid = verify_webhook_signature(
request.data,
request.headers.get('X-Specora-Signature'),
request.headers.get('X-Specora-Timestamp'),
os.environ['WEBHOOK_SECRET'],
)
if not is_valid:
return jsonify({'error': 'Invalid signature'}), 401
payload = parse_webhook_payload(request.data)
if payload.event_type == 'platform.budget_breach':
# Handle budget breach
pass
elif payload.event_type == 'platform.invariant_failed':
# Handle policy violation
pass
return jsonify({'received': True})
Webhook Event Types
platform.invariant_failed- Governance invariant violationplatform.budget_breach- Budget limit exceededplatform.provider_quarantined- AI provider quarantinedplatform.drift_detected- Model or schema driftplatform.break_glass_used- Emergency override activatedplatform.sla_breach- SLA threshold violatedplatform.risk_score_changed- Significant risk change
Error Handling
The SDK throws typed exceptions for different failure scenarios:
from specora import (
SpecoraClient,
FailClosedError,
BudgetExceededError,
AuthenticationError,
ValidationError,
)
try:
result = client.check_policy(...)
except FailClosedError as e:
# Network/server error in fail-closed mode
print(f"Defaulting to blocked: {e.decision}")
except BudgetExceededError as e:
# Budget exceeded
print(f"Budget exceeded: {e.usage_percent}%")
except AuthenticationError:
# Invalid API key
pass
except ValidationError as e:
# Invalid request parameters
print(f"Validation errors: {e.validation_errors}")
Security
HTTPS Required
The SDK enforces HTTPS for all non-local base_url values. Plaintext HTTP
would expose your API key (sent as a Bearer token on every request) to
network observers.
# OK - production
SpecoraClient(base_url="https://api.specora.ai", ...)
# OK - local development
SpecoraClient(base_url="http://localhost:8765", ...)
SpecoraClient(base_url="http://127.0.0.1:8765", ...)
# RAISES ValueError - plaintext HTTP to remote host
SpecoraClient(base_url="http://api.specora.ai", ...)
Supply Chain
- Minimal runtime dependencies — only
httpxandpydantic. - No install-time scripts —
pip installexecutes no lifecycle hooks. - Fail-closed default — network/server errors block operations, not allow them.
Network Hardening
- No redirects — httpx is configured with
follow_redirects=Falseto prevent theAuthorizationheader from leaking to redirect targets. - Response size cap — responses over 10 MB are rejected to prevent OOM from a malicious or compromised server.
- Retry-After cap — server-provided
Retry-Aftervalues are capped at 120 seconds to prevent indefinite blocking.
Credential Handling
- API keys are only transmitted in the
Authorizationheader over HTTPS. - Keys are never logged or serialized to error messages.
repr(client)returnsSpecoraClient('https://...')— no API key exposed.- Webhook verification uses constant-time comparison (
hmac.compare_digest) to prevent timing attacks. - Empty webhook secrets are rejected to prevent signature forgery.
Governance Router Safety
- Message size limits — max 1024 messages and 10 MB total content per request.
- Authorization expiry — checked before LLM execution to prevent stale authorizations.
- Conservative token estimation — uses chars/3 (not chars/4) to prevent budget overspend.
- Recording failures surfaced —
recording_failed_warningfield on response instead of silent error swallowing.
Requirements
- Python 3.10+
- httpx
- pydantic
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file specora-0.5.0.tar.gz.
File metadata
- Download URL: specora-0.5.0.tar.gz
- Upload date:
- Size: 46.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b4484adf9837016418ee39efc5770ac172e35bac1b7c4b4911d6ac2c70eb10f
|
|
| MD5 |
65bdf380dd43033d337a0b772812a6a3
|
|
| BLAKE2b-256 |
233508792cf5d1df21347b72f0e921a5330c34ddfd49af13bea42054a7de7a85
|
Provenance
The following attestation bundles were made for specora-0.5.0.tar.gz:
Publisher:
sdk-python-publish.yml on SpecoraAI/specora-platform
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
specora-0.5.0.tar.gz -
Subject digest:
6b4484adf9837016418ee39efc5770ac172e35bac1b7c4b4911d6ac2c70eb10f - Sigstore transparency entry: 1803932375
- Sigstore integration time:
-
Permalink:
SpecoraAI/specora-platform@ba6433800970d13f4cff16e05149b17b1a0926b2 -
Branch / Tag:
refs/tags/sdk-python-specora-v0.5.0 - Owner: https://github.com/SpecoraAI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
sdk-python-publish.yml@ba6433800970d13f4cff16e05149b17b1a0926b2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file specora-0.5.0-py3-none-any.whl.
File metadata
- Download URL: specora-0.5.0-py3-none-any.whl
- Upload date:
- Size: 54.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b19bc630570e949f41b12d717019824957827936d7660284d01a9505498a45d7
|
|
| MD5 |
c909ef5ec434ca92791c3170d71aa487
|
|
| BLAKE2b-256 |
3b4f7346c476d9855ba477ec46d99089ed0ba8ade370855501c6c5d2722949ad
|
Provenance
The following attestation bundles were made for specora-0.5.0-py3-none-any.whl:
Publisher:
sdk-python-publish.yml on SpecoraAI/specora-platform
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
specora-0.5.0-py3-none-any.whl -
Subject digest:
b19bc630570e949f41b12d717019824957827936d7660284d01a9505498a45d7 - Sigstore transparency entry: 1803932519
- Sigstore integration time:
-
Permalink:
SpecoraAI/specora-platform@ba6433800970d13f4cff16e05149b17b1a0926b2 -
Branch / Tag:
refs/tags/sdk-python-specora-v0.5.0 - Owner: https://github.com/SpecoraAI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
sdk-python-publish.yml@ba6433800970d13f4cff16e05149b17b1a0926b2 -
Trigger Event:
push
-
Statement type: