Documentation

Learn how to install and use the D2 Python SDK

The D2 Python SDK reduces the complexity of implementing enterprise-grade authorization for AI agents. Add authorization to any Python function with a single decorator.

Installation and setup

Install the SDK

bash
pip install d2-python

Initialize D2

After installation, initialize D2 at application startup. Here are examples for different frameworks:

python
# main.py
from fastapi import FastAPI, Depends
from contextlib import asynccontextmanager
import d2

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Initialize D2 on startup
    await d2.configure_rbac_async()
    yield
    # Cleanup on shutdown
    await d2.shutdown_rbac()

app = FastAPI(lifespan=lifespan)

# Add D2 middleware for automatic context management
app.add_middleware(d2.ASGIMiddleware)

@app.get("/weather")
async def get_weather(location: str):
    # Context automatically set by middleware from headers
    return await weather_service.fetch(location)

Protect your functions

You can protect functions in two ways: with custom tool IDs or auto-detected IDs. The following examples show both approaches:

Custom Tool ID
python
@d2.d2_guard("weather_api")
def get_weather(location: str):
    return weather_service.fetch(location)

Recommended: Use descriptive names for better policy organization.

Auto-detected ID
python
@d2.d2_guard()
def send_email(recipient: str):
    # Tool ID: myapp.send_email
    return email_service.send(recipient)

Zero config: Uses module.qualname format - the module path plus the qualified name (including nested classes - see below).

🔧

Auto-detected ID Format

When you omit the first argument, D2 automatically generates a stable tool ID using module.qualname - the module path combined with the qualified name, which includes the full path through any nested classes or functions:

Declaration
python
def health_check(): ...

class UserApi:
    def get(self): ...
Generated ID
text
myapp.health_check

myapp.UserApi.get

Generate your policy

D2 automatically scans your codebase and creates a policy template with all detected functions:

bash
python -m d2 init
🔍

Code Analysis Feature

The d2 init command analyzes your Python files, finds all @d2_guard decorators, and automatically generates a policy template with the detected tool IDs.

Visual Code-to-Policy Mapping

Your Code
@d2_guard("weather_api")
def get_weather(): pass
@d2_guard()
def send_email(): pass
📋
Generated Policy
permissions:
- "weather_api"
- "myapp.send_email"
~/.config/d2/policy.yaml
metadata:
  name: "my-app"
  expires: "2025-02-01T00:00:00Z"

policies:
  - role: admin
    permissions: ["*"]
  
  - role: developer
    permissions:
      - "weather_api"        # Custom ID from your code
      - "myapp.send_email"   # Auto-detected ID

Error handling

The D2 SDK provides flexible error handling options. You can customize how authorization denials are handled using the on_deny parameter:

python
@d2.d2_guard("admin_only")
def delete_user(user_id: str):
    return user_service.delete(user_id)

# Handle the exception
try:
    delete_user("user123")
except d2.PermissionDeniedError as e:
    print(f"Access denied: {e}")
    return {"error": "Insufficient permissions"}

Default behavior: Raises PermissionDeniedError when access is denied.

SDK Exceptions

The D2 SDK raises specific exceptions for different failure modes. All exceptions inherit from D2Error:

ExceptionWhen Raised
PermissionDeniedErrorUser lacks required permissions for the protected function
MissingPolicyErrorNo policy bundle available (cloud mode network issues or local file missing)
BundleExpiredErrorPolicy bundle has expired and no fresh bundle is available
D2PlanLimitErrorAccount has reached plan limits (tool count, features, or trial expired)
TooManyToolsErrorPolicy exceeds the maximum number of tools allowed for your plan
PolicyTooLargeErrorPolicy bundle exceeds size limits
InvalidSignatureErrorPolicy bundle signature verification failed (cloud mode)
ConfigurationErrorInvalid configuration or environment setup
D2NoContextErrorNo user context available when calling a protected function
💡

Production Tip: Catch D2PlanLimitError and BundleExpiredError to implement graceful degradation. For example, log the error and continue with limited functionality rather than crashing.

Context management

!

Context Leak Warning

Always clear context or use context managers. Leaked context can cause security issues where subsequent operations run with wrong user permissions.

python
# ✅ Recommended: Context manager automatically clears context
with d2.set_user_context("alice", ["admin"]):
    result = protected_function()
    # Context automatically cleared when exiting block

# ✅ Also safe: Using run_as helper
with d2.run_as("bob", ["viewer"]):
    data = get_user_data()

✅ Safe: Context is automatically cleared when the block exits.

Security Note: Context Management Options

Recommended: Use with d2.set_user_context() for automatic cleanup.

Alternative: When context managers aren't suitable, you can use d2.set_user() followed by d2.clear_user_context() in a finally block.

⚠️ Never use d2.set_user() without proper cleanup to prevent context leaks.

Context Flow Rules

Context flows automatically through:
  • await async_function()
  • asyncio.create_task(task)
  • anyio.to_thread.run_sync(func)
Context does NOT flow through:
  • threading.Thread(target=func)
  • multiprocessing.Process()
  • Manual threads and processes

CLI commands

Policy Management

bash
python -m d2 init

Generate policy template with auto-detected functions

Options
--path PATH Custom output path
--format FORMAT Output format (yaml, json)
--force Overwrite existing policy
bash
python -m d2 diagnose

Validate policy structure and limits

bash
python -m d2 inspect

Pretty-print active bundle and verify signatures

Options
-v, --verbose Show detailed information

Cloud Operations

bash
python -m d2 draft

Upload policy draft to cloud (requires D2_TOKEN)

bash
python -m d2 publish

Sign and publish policy for production

bash
python -m d2 pull

Download cloud policy to local file

Options
-o OUTPUT, --output OUTPUT Output file path (default: ./policy.yaml)
--format {yaml,json} Force output format when downloading cloud policy (default: yaml)
--app-name APP_NAME Specify which app's policy to fetch (overrides local policy file)
--stage {published,draft} Which version to fetch: published (stable) or draft (work-in-progress)
bash
python -m d2 switch <app>

Switch between apps and sync policies

bash
python -m d2 status

Show current app context, bundle info, and cache status

Environment variables

VariablePurpose
D2_TOKENOpaque D2 token (d2_...). Enables cloud mode.
D2_POLICY_FILECustom path to your policy YAML/JSON file when running in local mode (default: ~/.config/d2/policy.yaml)
D2_TELEMETRYControls what telemetry data is sent: off (none), metrics (OpenTelemetry only), usage (usage events only), all (everything, default)
D2_API_URLBase URL for the D2 control plane API (default: https://d2.artoo.love)
D2_STRICT_SYNCSet to "1" to raise an error when sync functions are called from async context, instead of automatically running them in a thread pool
D2_JWKS_URLOverride JWKS endpoint for signature verification (rare; cloud mode usually auto-discovers)
D2_STATE_PATHOverride persisted bundle state path (default: ~/.config/d2/bundles.json); use :memory: to disable persistence
D2_SILENTSet to "1" to suppress startup banner and expiry warnings in local mode
OTEL_*Standard OpenTelemetry configuration variables (e.g., OTEL_EXPORTER_OTLP_ENDPOINT) for metrics export

Threading & Async

D2 provides special threading utilities for cross-thread context management:

python
from concurrent.futures import ThreadPoolExecutor
import d2

# Use ambient context (snapshot current user)
d2.set_user("alice", ["admin"])
executor = ThreadPoolExecutor()
future = d2.threads.submit_with_context(executor, some_guarded_tool, arg1)
result = future.result()  # Runs as alice

# Use explicit actor (preferred for sensitive operations)
actor = d2.UserContext(user_id="bob", roles=frozenset(["viewer"]))
future = d2.threads.submit_with_context(executor, sensitive_tool, actor=actor)
result = future.result()  # Runs as bob, regardless of ambient context

Caller controls thread: Submit work to thread pools with proper context isolation.

Security Guarantees

  • Fail-closed by Default – Every background thread is guaranteed to run with a user context. If no context is available (neither ambient nor explicit), the operation will fail rather than running with anonymous permissions.
  • Automatic Cleanup – The user context is automatically cleared when the thread finishes, preventing context from leaking into subsequent tasks that might reuse the same worker thread.
  • Confused Deputy Protection – To prevent confused deputy attacks, passing an explicit actor to submit_with_context always overrides any ambient user context. The task runs as the actor, not the submitter.
  • Strict Actor Enforcement – For sensitive operations, @thread_entrypoint(require_actor=True) enforces that the function can only be called with an explicit actor, rejecting calls that rely on ambient context.

System architecture

D2 Cloud Platform Architecture

Learn about D2's cloud-native authorization platform, policy lifecycle, security model, token management, and dashboard integration.

View System Architecture

Source code

The D2 Python SDK is open source and available on GitHub:

D2-Python

Enterprise-grade authorization for AI agents

View on GitHub