data_classification.rego
Cross-cutting access control based on a data_classification label attached to an event. Four tiers, with default-deny outside the explicitly allowed combinations.
This is not a separate event type — data_classification is a field on every GovernanceEvent, so the same policy gates a tool_call that carries confidential data, an agent.delegate that would forward restricted data, etc. See governance events for the full event shape.
Package: kitelogik.data_classification · Source: kitelogik/policies/data_classification.rego
The four tiers
| Tier | Allowed when | Denied when |
|---|---|---|
null (no label) | Always | — |
"public" | Always | — |
"internal" | Any session | — |
"confidential" | delegation_depth == 0 (primary session) | delegation_depth > 0 |
"restricted" | Session has restricted_data scope | Scope missing |
Anything outside these labels falls through to the default allow := false — treat unknown classifications as deny.
Source
package kitelogik.data_classification
import future.keywords.if
import future.keywords.in
default allow := false
default deny := false
allow if { input.data_classification == null }
allow if { input.data_classification == "public" }
allow if { input.data_classification == "internal" }
allow if {
input.data_classification == "confidential"
input.context.delegation_depth == 0
}
allow if {
input.data_classification == "restricted"
"restricted_data" in input.context.session_scopes
}
deny if {
input.data_classification == "confidential"
input.context.delegation_depth > 0
}
deny if {
input.data_classification == "restricted"
not "restricted_data" in input.context.session_scopes
}How main.rego uses this module
Both allow and deny propagate, regardless of event type:
allow if { not deny; data_classification.allow }
deny if { data_classification.deny }This is intentional — data classification flow control should apply everywhere data moves, not be tied to a specific event type.
Setting data_classification on an event
The runtime (or an adapter) attaches the classification when it knows the data tier. For example, a tool that returns confidential data:
from kitelogik.tether.models import GovernanceEvent, ToolCallInput
event = GovernanceEvent(
event_type="tool_call",
session_id=context.session_id,
action="get_customer_pii",
tool_name="get_customer_pii",
args={"customer_id": "c1"},
context=context,
data_classification="confidential", # ← key field
)
decision = await gate.evaluate(event)For most tools this label is set declaratively next to the tool definition — your adapter can attach it automatically based on the tool's known data tier rather than at the call site.
Common patterns
Block confidential data from reaching a third-party tool. Combine this module with a custom tool-call rule:
package kitelogik.financial # any package — denies merge in main.rego
import future.keywords.if
deny if {
input.tool_name in {"send_to_external_api", "post_to_webhook"}
input.data_classification in {"confidential", "restricted"}
}Strict tenant isolation for restricted data.
package kitelogik.data_classification
import future.keywords.if
deny if {
input.data_classification == "restricted"
input.context.tenant_id != input.args.target_tenant_id
}Extending the tier set
The OSS module ships four tiers. To add a fifth (e.g. "highly_sensitive"), add same-package allow + deny rules:
package kitelogik.data_classification
import future.keywords.if
import future.keywords.in
allow if {
input.data_classification == "highly_sensitive"
input.context.user_role == "compliance_officer"
"highly_sensitive" in input.context.session_scopes
}
deny if {
input.data_classification == "highly_sensitive"
not (input.context.user_role == "compliance_officer"
and "highly_sensitive" in input.context.session_scopes)
}Both rules are necessary — Rego's default allow := false doesn't imply default deny := true. Without the explicit deny, an unhandled classification would just produce no decision (resolves to allow=False, deny=False, which surfaces as a soft deny / HITL candidate in main.rego).