Principals¶
A principal carries identity context -- who initiated the tool call, what role they have, what ticket authorized it. Edictum does not authenticate principals. Your application sets the principal, and contracts evaluate against it.
When to use this¶
Read this page when you need to attach identity context to tool calls -- role-based permissions, tenant isolation, or compliance attribution. Principals carry fields like role, user_id, org_id, and ticket_ref that contracts can evaluate against and that appear on every audit event. OTel spans include role, ticket_ref, user_id, and org_id as edictum.principal.* attributes. Note: service_id is a Principal field but is not emitted to OTel spans. The code also checks for team in the principal dict, but since team is not a named Principal field it will only appear if the application explicitly adds it to the dict. For contracts that use principal fields, see contracts. For how principals flow through the pipeline, see how it works.
Principal Fields¶
from edictum import Principal
principal = Principal(
user_id="alice",
service_id="billing-agent",
org_id="acme-corp",
role="analyst",
ticket_ref="INC-1234",
claims={"department": "finance", "clearance": "confidential"},
)
| Field | Type | Description |
|---|---|---|
user_id |
str or None |
The human or service account that initiated the session |
service_id |
str or None |
The agent or service making the tool call |
org_id |
str or None |
Organization or tenant identifier |
role |
str or None |
Role used for role-based contract conditions |
ticket_ref |
str or None |
Change management ticket (Jira, ServiceNow, PagerDuty) |
claims |
dict |
Arbitrary key-value pairs for custom authorization context |
All fields are optional. Use only what your contracts need.
Attaching a Principal¶
Pass the principal when creating an adapter. It is carried through every tool call and audit event in that session.
from edictum import Edictum, Principal
from edictum.adapters.langchain import LangChainAdapter
guard = Edictum.from_yaml("contracts.yaml")
principal = Principal(role="analyst", ticket_ref="INC-1234")
adapter = LangChainAdapter(guard=guard, principal=principal)
wrapper = adapter.as_tool_wrapper()
Or use Edictum.run() directly:
result = await guard.run(
"query_db",
{"query": "SELECT * FROM users"},
query_fn,
principal=principal,
)
Using Principals in Contracts¶
Contracts reference principal fields through the principal.* selectors.
Require a ticket for non-admin writes¶
- id: require-ticket-for-writes
type: pre
tool: "*"
when:
all:
- principal.role: { not_in: [admin, sre] }
- principal.ticket_ref: { exists: false }
then:
effect: deny
message: "Non-admin tool calls require a ticket reference."
When a principal with role: "analyst" and no ticket_ref calls any tool, this contract fires. An admin or SRE can proceed without a ticket.
Gate production deploys by role¶
- id: prod-deploy-requires-senior
type: pre
tool: deploy_service
when:
all:
- environment: { equals: production }
- principal.role: { not_in: [senior_engineer, sre, admin] }
then:
effect: deny
message: "Production deploys require senior role (sre/admin)."
Use custom claims for fine-grained access¶
- id: only-platform-can-scale
type: pre
tool: scale_service
when:
principal.claims.department: { not_equals: platform }
then:
effect: deny
message: "Only the platform team can scale services."
The claims dict supports dotted path access: principal.claims.department resolves to principal.claims["department"]. If the key is missing, the condition evaluates to false and the contract does not fire.
Principal Propagation¶
Set the principal once at the adapter level. It propagates automatically to:
- Every
ToolEnvelopebuilt for each tool call - Every precondition and postcondition evaluation
- Every
AuditEventemitted by the pipeline - Every OpenTelemetry span (as
edictum.principal.*attributes forrole,ticket_ref,user_id, andorg_id-- note thatservice_idis not included in OTel spans; the code also checks forteambut it is not a named Principal field)
You do not need to pass the principal on each tool call. The adapter carries it for the entire session.
Missing Principal¶
If no principal is set:
- Contracts that check
principal.*fields seenullvalues. - A condition like
principal.role: { not_in: [admin] }evaluates tofalse(missing field behavior), so the contract does not fire. - A condition like
principal.ticket_ref: { exists: false }evaluates totrue, because the field is absent.
Design your contracts to handle the no-principal case explicitly if you need to enforce principal requirements:
- id: require-principal
type: pre
tool: "*"
when:
principal.user_id: { exists: false }
then:
effect: deny
message: "A principal with user_id is required for all tool calls."
Next Steps¶
- Contracts -- how to write preconditions, postconditions, and session contracts
- Adapters overview -- how to set a principal per framework
- YAML reference -- full selector and operator reference
- How it works -- where principal checks fit in the pipeline