Skip to main content

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

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 violation
  • platform.budget_breach - Budget limit exceeded
  • platform.provider_quarantined - AI provider quarantined
  • platform.drift_detected - Model or schema drift
  • platform.break_glass_used - Emergency override activated
  • platform.sla_breach - SLA threshold violated
  • platform.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 httpx and pydantic.
  • No install-time scriptspip install executes no lifecycle hooks.
  • Fail-closed default — network/server errors block operations, not allow them.

Network Hardening

  • No redirects — httpx is configured with follow_redirects=False to prevent the Authorization header 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-After values are capped at 120 seconds to prevent indefinite blocking.

Credential Handling

  • API keys are only transmitted in the Authorization header over HTTPS.
  • Keys are never logged or serialized to error messages.
  • repr(client) returns SpecoraClient('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 surfacedrecording_failed_warning field on response instead of silent error swallowing.

Requirements

  • Python 3.10+
  • httpx
  • pydantic

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

specora-0.4.0.tar.gz (43.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

specora-0.4.0-py3-none-any.whl (50.8 kB view details)

Uploaded Python 3

File details

Details for the file specora-0.4.0.tar.gz.

File metadata

  • Download URL: specora-0.4.0.tar.gz
  • Upload date:
  • Size: 43.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for specora-0.4.0.tar.gz
Algorithm Hash digest
SHA256 c2df9898568bfc6da3339fd8d62c0949f9897b04fddbeabca46467ee1fdcb12d
MD5 17cb9b0c4aabc05b6f21a9af53027385
BLAKE2b-256 572fa924fb346435623e1601301a74fd7e21362214dd58e48087c918dfc1cac9

See more details on using hashes here.

Provenance

The following attestation bundles were made for specora-0.4.0.tar.gz:

Publisher: sdk-python-publish.yml on SpecoraAI/specora-platform

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file specora-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: specora-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 50.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for specora-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 362eabdb91030a37c3b49f57d420f8be5061fa9933fd98c24f6089e5311a8620
MD5 01e5ce7a2392915a91c94affdf832056
BLAKE2b-256 4b6d9e23ae39a1ec2877779e5613fd590876d5e22d6514f8959a23cef3041ae1

See more details on using hashes here.

Provenance

The following attestation bundles were made for specora-0.4.0-py3-none-any.whl:

Publisher: sdk-python-publish.yml on SpecoraAI/specora-platform

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page