Policies¶
Policies are the core of OrmAI's security model. They define what agents can access and how data is protected.
Policy Structure¶
A complete policy includes model policies, budgets, and optional defaults:
from ormai.policy import (
Policy,
ModelPolicy,
FieldPolicy,
FieldAction,
Budget,
WritePolicy,
)
policy = Policy(
models={
"User": ModelPolicy(...),
"Order": ModelPolicy(...),
},
budget=Budget(
max_rows=1000,
max_include_depth=3,
statement_timeout_ms=5000,
),
default_field_action=FieldAction.Deny,
)
Model Policies¶
Model policies control access to specific database models:
ModelPolicy(
allowed=True, # Enable access to this model
fields={...}, # Field-level policies
relations={...}, # Relation policies
scoping={"tenant_id": "principal.tenant_id"}, # Auto-scoping
row_policies=[...], # Row-level security
write_policy=WritePolicy(...), # Write operation rules
)
Disabling a Model¶
Field Policies¶
Field policies control access to individual fields:
from ormai.policy import FieldPolicy, FieldAction
fields = {
"id": FieldPolicy(action=FieldAction.Allow),
"email": FieldPolicy(action=FieldAction.Mask),
"ssn": FieldPolicy(action=FieldAction.Hash),
"password_hash": FieldPolicy(action=FieldAction.Deny),
"notes": FieldPolicy(action=FieldAction.Redact),
}
Field Actions¶
| Action | Description | Output Example |
|---|---|---|
Allow |
Field is returned as-is | [email protected] |
Deny |
Field is completely hidden | (not in response) |
Mask |
Partially obscured | u***@***.com |
Hash |
Deterministic hash | a1b2c3d4... |
Redact |
Replaced with placeholder | [REDACTED] |
Custom Masking¶
Scoping Rules¶
Scoping automatically filters queries to the current principal's context:
ModelPolicy(
scoping={
"tenant_id": "principal.tenant_id", # Tenant isolation
"owner_id": "principal.user_id", # User-level access
}
)
How Scoping Works¶
When a query is executed:
# Original query
toolset.query(ctx, model="Order", filters=[...])
# After scope injection
SELECT * FROM orders
WHERE tenant_id = 'acme-corp' -- Injected from principal
AND <user filters>
Scoping Expressions¶
| Expression | Resolves To |
|---|---|
principal.tenant_id |
ctx.principal.tenant_id |
principal.user_id |
ctx.principal.user_id |
principal.roles |
ctx.principal.roles |
Row-Level Policies¶
For complex access control beyond simple scoping:
from ormai.policy import RowPolicy
ModelPolicy(
row_policies=[
RowPolicy(
name="draft_visibility",
condition="status != 'draft' OR owner_id = principal.user_id",
description="Users can only see their own drafts",
),
RowPolicy(
name="admin_access",
condition="'admin' IN principal.roles",
bypass=True, # Admins bypass other row policies
),
]
)
Relation Policies¶
Control access to related data:
from ormai.policy import RelationPolicy
ModelPolicy(
relations={
"orders": RelationPolicy(
allowed=True,
max_depth=2,
fields=["id", "status", "total"], # Limit included fields
),
"audit_logs": RelationPolicy(allowed=False), # Block relation
}
)
Write Policies¶
Control create, update, and delete operations:
from ormai.policy import WritePolicy, WriteAction
ModelPolicy(
write_policy=WritePolicy(
create=WriteAction.Allow,
update=WriteAction.RequireApproval, # Human approval needed
delete=WriteAction.Deny,
# Field-level write control
immutable_fields=["id", "created_at", "tenant_id"],
required_fields=["name", "email"],
# Auto-populate fields
auto_set={
"tenant_id": "principal.tenant_id",
"created_by": "principal.user_id",
},
)
)
Write Actions¶
| Action | Description |
|---|---|
Allow |
Operation is allowed |
Deny |
Operation is blocked |
RequireApproval |
Operation requires human approval |
Budgets¶
Budgets prevent expensive operations:
Budget(
max_rows=1000, # Maximum rows per query
max_include_depth=3, # Maximum relation nesting
max_selected_fields=50, # Maximum fields per query
statement_timeout_ms=5000, # Query timeout
max_complexity_score=100, # Computed query complexity
)
Complexity Scoring¶
OrmAI computes a complexity score for each query:
# Factors that increase complexity:
# - Number of filters
# - Include depth
# - Number of selected fields
# - Aggregations
# - Ordering
query = {
"model": "Order",
"filters": [...], # +2 per filter
"include": [ # +5 per include level
{"relation": "items", "include": [...]},
],
"order": [...], # +1 per order clause
}
Policy Builder¶
For fluent policy construction:
from ormai.utils import PolicyBuilder
policy = (
PolicyBuilder()
.add_model("User")
.allow_fields("id", "name", "email")
.mask_field("email")
.scope_by_tenant()
.done()
.add_model("Order")
.allow_fields("id", "status", "total", "created_at")
.allow_writes(create=True, update=True, delete=False)
.scope_by_tenant()
.done()
.set_budget(max_rows=500, max_include_depth=2)
.build()
)
Default Profiles¶
Use preset profiles for common scenarios:
from ormai.utils import DefaultsProfile
# Production: Strict defaults
policy = DefaultsProfile.DEFAULT_PROD.apply(base_policy)
# Internal tools: Relaxed defaults
policy = DefaultsProfile.DEFAULT_INTERNAL.apply(base_policy)
# Development: Permissive defaults
policy = DefaultsProfile.DEFAULT_DEV.apply(base_policy)
Policy Validation¶
Validate policies at startup:
from ormai.policy import validate_policy
errors = validate_policy(policy, schema_metadata)
for error in errors:
print(f"Policy error: {error}")
Common validation errors:
- Unknown model in policy
- Unknown field in field policy
- Invalid scoping expression
- Circular relation policies
Next Steps¶
- Adapters - How policies are applied by adapters
- Tools - Tools that use policies
- Audit Logging - Policy decisions in audit logs