Metadata-Version: 2.4
Name: dbl-policy
Version: 0.2.2
Summary: Deterministic policy evaluation layer for dbl-core
Author-email: Lukas Pfister <228201683+lukaspfisterch@users.noreply.github.com>
License: MIT
Project-URL: Repository, https://github.com/lukaspfisterch/dbl-policy
Project-URL: Issues, https://github.com/lukaspfisterch/dbl-policy/issues
Keywords: dbl,policy,governance,deterministic
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: dbl-core<0.4,>=0.3
Provides-Extra: test
Requires-Dist: pytest<9,>=8; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: ruff<1,>=0.5; extra == "dev"
Requires-Dist: mypy<2,>=1.10; extra == "dev"
Dynamic: license-file

# DBL Policy

Deterministic, tenant-scoped policy evaluation for DBL.
This package produces DECISION events only. It does not execute tasks.
Status: Stable

## What it is

`dbl-policy` is the normative gate in the DBL stack:

- It evaluates a policy from authoritative inputs only
- It returns ALLOW or DENY with stable reason codes
- It can be bridged into a `dbl-core` DECISION event with a strict, contract-shaped `data` mapping
- It is pure: no IO, no time, no randomness, no env, no network, no trace-dependence

## Non-goals

- No execution
- No orchestration
- No reading or depending on observational fields (trace, runtime, exceptions, etc.)
- No mutation of event streams

## Contract

The authoritative specification is in:
- `docs/dbl_policy_contract.md`

Key invariants enforced by this package:

- Inputs must be JSON-safe and deterministic
  - No floats (including nested)
  - Mapping keys must be exact `str` (no subclasses)
- Policy input is whitelisted
  - Unknown keys are rejected
- `PolicyContext` is snapshotted
  - Caller mutation after construction cannot affect evaluation or digest
- Output can be converted into a `dbl-core` DECISION event
  - `DECISION.data` is a Mapping with a strict shape
  - Includes policy lineage: `policy_id`, `policy_version`, `tenant_id`

## Install

```bash
pip install dbl-policy
```

Requires Python 3.11+ and `dbl-core>=0.3,<0.4`.

## Quickstart

1) Use a built-in policy

```python
from dbl_policy import PolicyContext, TenantId
from dbl_policy.allow_all import POLICY as ALLOW_ALL

ctx = PolicyContext(
    tenant_id=TenantId("tenant-1"),
    inputs={"intent_type": "chat.message"},
)

decision = ALLOW_ALL.evaluate(ctx)
```

2) Convert a decision to a DBL DECISION event

```python
from dbl_policy import decision_to_dbl_event

event = decision_to_dbl_event(decision, correlation_id="c1")
```

`event.data` will be shaped like:

```python
{
  "policy_id": "...",
  "policy_version": "...",
  "tenant_id": "...",
  "gate": {
    "decision": "ALLOW" | "DENY",
    "reason_code": "...",
    # "reason_message": "..." (optional)
  }
}
```

## Writing your own policy

Policies only implement `evaluate(context)` and must be deterministic.

```python
from dataclasses import dataclass
from dbl_policy import (
    PolicyContext,
    PolicyDecision,
    PolicyId,
    PolicyVersion,
    DecisionOutcome,
)
from dbl_policy import reason_codes


@dataclass(frozen=True)
class ExamplePolicy:
    policy_id: PolicyId = PolicyId("example")
    policy_version: PolicyVersion = PolicyVersion("1.0.0")

    def evaluate(self, context: PolicyContext) -> PolicyDecision:
        intent_type = context.inputs.get("intent_type")
        if intent_type == "blocked":
            return PolicyDecision(
                outcome=DecisionOutcome.DENY,
                reason_code=reason_codes.TENANT_BLOCKED,
                reason_message="blocked intent_type",
                policy_id=self.policy_id,
                policy_version=self.policy_version,
                tenant_id=context.tenant_id,
            )
        return PolicyDecision(
            outcome=DecisionOutcome.ALLOW,
            reason_code=reason_codes.OK,
            policy_id=self.policy_id,
            policy_version=self.policy_version,
            tenant_id=context.tenant_id,
        )
```

## Safe evaluation (recommended)

`decide_safe` wraps context validation and converts failures into a stable DENY.

- Valid inputs: evaluates policy, ensures `authoritative_digest` is populated
- Invalid inputs: DENY with stable reason_code
- Exceptions during evaluation: DENY with `evaluation_error`

```python
from dbl_policy import decide_safe
from dbl_policy.allow_all import POLICY

d1 = decide_safe(POLICY, "tenant-1", {"intent_type": "x"})
d2 = decide_safe(POLICY, "tenant-1", {"unknown_key": "x"})  # -> DENY
```

## Starter policy pack

The minimal starter pack is in `dbl_policy.policies.compose` and is designed for
small teams. It evaluates in order and stops at the first DENY.

Allowed context keys (strict whitelist):
- principal_id
- workspace_id
- intent_type
- capability
- model_id
- provider
- max_output_tokens
- input_bytes
- input_chars
- risk_tier
- request_tags
- extensions

Policy configuration lives in a single static dict:
`dbl_policy.policies.compose.POLICY_CONFIG`.
Edit it in-code to change allowlists and caps (no IO or env).

## Reason codes

Reason codes are stable semantic identifiers:

- ok
- allow_all
- deny_all
- invalid_input
- unknown_context_key
- tenant_blocked
- missing_required_input
- evaluation_error
- admission.missing_required
- admission.invalid_value
- capability.denied
- model.denied
- cost.output_tokens_cap
- cost.input_bytes_cap
- cost.input_chars_cap
- risk.high_requires_override

See `src/dbl_policy/reason_codes.py`.

## Development

```bash
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install -e ".[dev]"
pytest
```
