Tenuo Constraints
How to use constraints to scope authority precisely.
Overview
Constraints are key-value pairs that restrict what a warrant authorizes. When a tool is invoked, Tenuo checks that the arguments satisfy the warrant’s constraints.
from tenuo import Warrant, Pattern, Range
# Create warrant with per-tool constraints
warrant = (Warrant.mint_builder()
.capability("read_file",
path=Pattern("/data/*"), # Path must match /data/*
max_size=Range.max_value(1000) # Size must be ≤ 1000
)
.holder(worker_pubkey)
.ttl(3600)
.mint(key))
# Tool invocation checks constraints
@guard(tool="read_file")
def read_file(path: str):
# This code only runs if path matches the warrant constraint
with open(path) as f:
return f.read()[:1000]
Closed-World Mode (Trust Cliff)
When you define any constraint on a tool, Tenuo activates closed-world mode for that capability: arguments not explicitly constrained are rejected by default.
This is a security feature—once you start defining what’s allowed, Tenuo assumes you want strict enforcement.
The Trust Cliff
| Constraint State | Behavior |
|---|---|
| No constraints (empty) | OPEN: Any arguments allowed |
| ≥1 constraint defined | CLOSED: Unknown arguments rejected |
_allow_unknown=True |
Explicit opt-out from closed-world |
Example
from tenuo import Warrant, Pattern
# ❌ One constraint → unknown fields rejected
warrant = (Warrant.mint_builder()
.capability("api_call", url=Pattern("https://api.example.com/*"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# This FAILS - 'timeout' is not in the constraint set
api_call(url="https://api.example.com/v1", timeout=30)
# Error: "unknown field not allowed (zero-trust mode)"
Opt-Out with _allow_unknown
Use _allow_unknown=True to explicitly allow unconstrained fields:
# ✅ Opt-out: allow unknown fields
warrant = (Warrant.mint_builder()
.capability("api_call",
url=Pattern("https://api.example.com/*"),
_allow_unknown=True)
.holder(key.public_key)
.ttl(3600)
.mint(key))
# This SUCCEEDS - 'timeout' is allowed through
api_call(url="https://api.example.com/v1", timeout=30)
Explicitly Allow Specific Fields
Use Wildcard() to allow any value for a specific field while keeping closed-world mode:
from tenuo import Warrant, Pattern, Wildcard
# Constrain 'url', allow any 'timeout', reject other unknown fields
warrant = (Warrant.mint_builder()
.capability("api_call",
url=Pattern("https://api.example.com/*"),
timeout=Wildcard()) # Any value OK
.holder(key.public_key)
.ttl(3600)
.mint(key))
# ✅ ALLOWED - both fields are constrained
api_call(url="https://api.example.com/v1", timeout=30)
# ❌ BLOCKED - 'retries' is unknown
api_call(url="https://api.example.com/v1", timeout=30, retries=3)
[!IMPORTANT]
_allow_unknownis NOT inherited during attenuation.When you delegate (attenuate) a warrant, the child defaults to closed-world mode even if the parent had
_allow_unknown=True. This prevents privilege escalation through delegation.# Parent: open to unknown fields parent = (Warrant.mint_builder() .capability("api_call", url=Pattern("https://*"), _allow_unknown=True) .mint(key)) # Child: defaults to closed (even though parent was open) child = (parent.grant_builder() .capability("api_call", url=Pattern("https://api.example.com/*")) .grant(key)) # Child CANNOT enable _allow_unknown if parent had it disabled # This would fail: child cannot be more permissive than parent
Constraint Types
Wildcard
Matches anything. The universal superset that can be attenuated to any other constraint type.
from tenuo import Wildcard
# Allows any value
Wildcard()
Use case: Root warrants that grant broad authority, which can be narrowed later.
# Parent: any query
with mint(Capability("search", query=Wildcard())):
...
# Child: narrowed to specific pattern
with grant(Capability("search", query=Pattern("*public*"))):
...
Security: Wildcard can only appear in root warrants. Attenuating to Wildcard is blocked (would re-widen authority).
[!NOTE]
Wildcard()is different fromPattern("*"). See the Pattern section below for details.
Pattern (Glob)
Matches strings against Unix shell-style glob patterns.
from tenuo import Pattern
# Suffix wildcard - matches paths starting with /data/
Pattern("/data/*")
# Prefix wildcard - matches emails ending with @company.com
Pattern("*@company.com")
# Middle wildcard - matches specific file in any subdirectory
Pattern("/data/*/config.yaml")
# Single character - matches file1.txt, fileA.txt, etc.
Pattern("file?.txt")
# Character class - matches env-prod, env-staging, env-dev
Pattern("env-[psd]*")
# Exact match (no wildcard)
Pattern("specific-value")
Supported Glob Syntax:
| Syntax | Description | Example |
|---|---|---|
* |
Matches any characters (including none) | staging-* matches staging-web |
? |
Matches exactly one character | file?.txt matches file1.txt |
[abc] |
Matches any character in set | [psd]* matches prod, staging, dev |
[!abc] |
Matches any character NOT in set | [!0-9]* matches non-numeric start |
Examples by Wildcard Position:
| Pattern | Value | Match? | Description |
|---|---|---|---|
/data/* |
/data/file.txt |
Yes | Suffix wildcard |
/data/* |
/etc/passwd |
No | Wrong prefix |
*@company.com |
cfo@company.com |
Yes | Prefix wildcard |
*@company.com |
hacker@evil.com |
No | Wrong suffix |
/data/*/file.txt |
/data/reports/file.txt |
Yes | Middle wildcard |
/data/*/file.txt |
/data/reports/other.txt |
No | Filename mismatch |
file?.txt |
file1.txt |
Yes | Single char wildcard |
file?.txt |
file12.txt |
No | Too many chars |
[!IMPORTANT] Distinction:
Wildcard()vsPattern("*")vs"*"These three are NOT the same:
Wildcard()- Universal constraint that can be attenuated to any other constraint type (Pattern, Exact, Range, etc.)Pattern("*")- A glob pattern that matches any string, but can only be attenuated to other patterns or Exact"*"(string literal) - Just a regular string value that happens to contain an asterisk# Flexible: Wildcard can become anything with mint(Capability("search", query=Wildcard())): with grant(Capability("search", query=Pattern("/data/*"))): # OK ... with grant(Capability("search", query=Range.max_value(100))): # OK ... # Limited: Pattern can only narrow to other patterns or exact values with mint(Capability("search", query=Pattern("*"))): with grant(Capability("search", query=Pattern("/data/*"))): # OK (simple prefix) ... with grant(Capability("search", query=Exact("specific"))): # OK ... with grant(Capability("search", query=Range.max_value(100))): # FAILS - Type mismatch ...Best Practice: Use
Wildcard()in root warrants for maximum flexibility. UsePattern("*")only if you specifically need glob matching semantics.Attenuation Rules for Pattern:
- Suffix wildcard patterns (
/data/*) can narrow to longer prefixes (/data/reports/*)- Prefix wildcard patterns (
*@company.com) can narrow to exact values (cfo@company.com)- Patterns can always narrow to
Exact()if the value matches the pattern- Complex patterns with multiple wildcards can be narrowed if contained within parent
Exact
Matches exactly one value.
from tenuo import Exact
# Only allows "production"
Exact("production")
# Only allows this specific ID
Exact("user-12345")
OneOf
Matches any value in a set.
from tenuo import OneOf
# Allows any of these environments
OneOf(["staging", "production", "dev"])
# Allows specific actions
OneOf(["read", "list"])
Range
Constrains numeric values to a range.
from tenuo import Range
# 0 to 100 (inclusive)
Range(min=0, max=100)
# At most 1000
Range.max_value(1000)
# At least 10
Range.min_value(10)
[!WARNING] Precision Limit: Bounds are stored as 64-bit floats. Integers larger than 2^53 (9,007,199,254,740,992) will lose precision.
For Snowflake IDs or large 64-bit integers, use
ExactorPatternconstraints on their string representation instead. Do not useRangefor values > 2^53.
Examples:
| Range | Value | Match? |
|---|---|---|
Range.max_value(100) |
50 |
Yes |
Range.max_value(100) |
150 |
No |
Range(min=10, max=50) |
25 |
Yes |
Range(min=10, max=50) |
5 |
No |
Cidr
Constrains IP addresses to a network range using CIDR notation. Supports both IPv4 and IPv6.
from tenuo import Cidr
# IPv4 networks
Cidr("10.0.0.0/8") # 10.x.x.x
Cidr("192.168.0.0/16") # 192.168.x.x
Cidr("192.168.1.0/24") # 192.168.1.x
# IPv6 networks
Cidr("2001:db8::/32")
Examples:
| Cidr | IP | Match? |
|---|---|---|
Cidr("10.0.0.0/8") |
"10.1.2.3" |
Yes |
Cidr("10.0.0.0/8") |
"192.168.1.1" |
No |
Cidr("192.168.1.0/24") |
"192.168.1.100" |
Yes |
Cidr("192.168.1.0/24") |
"192.168.2.1" |
No |
Use case: Restrict API calls to internal networks, validate source IPs.
from tenuo import Warrant, ConstraintSet, Cidr
# Only allow requests from internal network
cs = ConstraintSet()
cs.insert("source_ip", Cidr("10.0.0.0/8"))
warrant = (Warrant.mint_builder()
.capability("api_call", cs)
.holder(kp.public_key)
.ttl(3600)
.mint(kp))
Attenuation: Child CIDR must be a subnet of parent.
# Parent: 10.0.0.0/8 (all 10.x.x.x)
parent = Cidr("10.0.0.0/8")
# Valid child: 10.1.0.0/16 (narrower)
child = Cidr("10.1.0.0/16") # OK - Subnet of parent
# Invalid child: 192.168.0.0/16 (different network)
child = Cidr("192.168.0.0/16") # FAILS - Not a subnet
UrlPattern
Validates URLs against scheme, host, port, and path patterns. Provides structured URL validation with proper parsing and normalization - safer than using Pattern or Regex for URL matching.
from tenuo import UrlPattern
# Match HTTPS URLs to specific host
UrlPattern("https://api.example.com/*")
# Any scheme (HTTP or HTTPS)
UrlPattern("*://api.example.com/*")
# Wildcard subdomain
UrlPattern("https://*.example.com/*")
# Specific port
UrlPattern("https://api.example.com:8443/*")
# Specific path prefix
UrlPattern("https://api.example.com/api/v1/*")
Pattern Components:
| Component | Syntax | Description |
|---|---|---|
| Scheme | https://, *:// |
Required. Use * for any scheme. |
| Host | api.example.com, *.example.com |
Required. Supports * prefix for subdomains. |
| Port | :8443 |
Optional. Omit for default port. |
| Path | /api/*, /v1/users |
Optional. Supports glob patterns. |
Examples:
| Pattern | URL | Match? |
|---|---|---|
UrlPattern("https://api.example.com/*") |
"https://api.example.com/v1/users" |
Yes |
UrlPattern("https://api.example.com/*") |
"http://api.example.com/v1" |
No (wrong scheme) |
UrlPattern("https://*.example.com/*") |
"https://www.example.com/home" |
Yes |
UrlPattern("https://*.example.com/*") |
"https://evil.com/home" |
No (wrong domain) |
UrlPattern("https://api.example.com:8443/*") |
"https://api.example.com:443/v1" |
No (wrong port) |
Use case: Restrict API calls to specific endpoints, enforce HTTPS, limit to trusted domains.
from tenuo import Warrant, ConstraintSet, UrlPattern
# Only allow HTTPS calls to internal API
cs = ConstraintSet()
cs.insert("endpoint", UrlPattern("https://api.internal.com/v1/*"))
warrant = (Warrant.mint_builder()
.capability("api_call", cs)
.holder(kp.public_key)
.ttl(3600)
.mint(kp))
Attenuation Rules:
- Scheme: Can narrow (any -> https) but not widen (https -> http)
- Host: Can narrow (*.example.com -> api.example.com) but not widen
- Port: Can add restriction but not remove
- Path: Can narrow (/api/* -> /api/v1/*) but not widen
# Parent: any subdomain, any path
parent = UrlPattern("https://*.example.com/*")
# Valid children
child = UrlPattern("https://api.example.com/*") # OK - Specific host
child = UrlPattern("https://api.example.com/v1/*") # OK - Specific host + path
# Invalid children
child = UrlPattern("http://api.example.com/*") # FAILS - Different scheme
child = UrlPattern("https://*.other.com/*") # FAILS - Different domain
Regex
Matches strings against regular expressions.
from tenuo import Regex
# Matches production-* pattern
Regex(r"^production-[a-z]+$")
# Matches email format
Regex(r"^[a-z]+@company\.com$")
[!WARNING] Attenuation Limitation
Regex constraints cannot be narrowed during attenuation.
from tenuo import Warrant, Regex, Exact # Parent with regex parent = (Warrant.mint_builder() .capability("query", env=Regex(r"^(staging|dev)-.*$")) .holder(key.public_key) .ttl(3600) .mint(key)) # Cannot narrow to different regex (even if provably narrower) child = (parent.grant_builder() .capability("query", env=Regex(r"^staging-.*$")) # FAILS .grant(key)) # Can narrow to Exact value (if it matches parent regex) child = (parent.grant_builder() .capability("query", env=Exact("staging-web")) # OK .grant(key)) # Can keep same regex pattern child = (parent.grant_builder() .capability("query", env=Regex(r"^(staging|dev)-.*$")) # OK .grant(key))Workaround: If you need to narrow regex constraints during delegation:
- Use
Pattern()instead (supports simple prefix/suffix narrowing)- Attenuate to
Exact()for specific values- Keep the same regex in child warrants
NotOneOf
Excludes specific values (use sparingly - prefer allowlists).
from tenuo import NotOneOf
# Block admin and root
NotOneOf(["admin", "root"])
Security: Always prefer OneOf (allowlist) over NotOneOf (denylist). NotOneOf should only be used to “carve holes” in a parent’s positive constraint.
Contains
List must contain all specified values.
from tenuo import Contains
# List must include both "read" and "write"
Contains(["read", "write"])
Example:
from tenuo import Warrant, Contains
# Warrant requires ["read", "write"] permissions
warrant = (Warrant.mint_builder()
.capability("access_resource", permissions=Contains(["read", "write"]))
.holder(key.public_key)
.ttl(3600)
.mint(key)
)
# Matches: ["read", "write", "admin"]
# Doesn't match: ["read"] (missing "write")
Subset
List must be a subset of allowed values.
from tenuo import Subset
# List must only contain allowed values
Subset(["staging", "dev", "test"])
Example:
from tenuo import Warrant, Subset
# Warrant allows only specific environments
warrant = (Warrant.mint_builder()
.capability("deploy", environments=Subset(["staging", "dev"]))
.holder(key.public_key)
.ttl(3600)
.mint(key)
)
# Matches: ["staging"]
# Matches: ["staging", "dev"]
# Doesn't match: ["staging", "production"] (includes disallowed "production")
All (AND)
All nested constraints must match.
from tenuo import All, Pattern, Range
# Path must match pattern AND size must be in range
All([
Pattern("/data/*"),
Range.max_value(1000)
])
Use case: Combine multiple constraint types for the same parameter.
AnyOf (OR)
At least one nested constraint must match.
from tenuo import AnyOf, Pattern
# Path must match at least one pattern
AnyOf([
Pattern("/data/reports/*"),
Pattern("/data/analytics/*")
])
[!NOTE]
AnyvsAnyOf: These are different!
AnyOf([...])- OR composite: at least one constraint must matchAny()- Alias forWildcard(): allows any value for a specific field in zero-trust mode
Not
Negation of a constraint.
from tenuo import Not, Exact
# Anything except "production"
Not(Exact("production"))
Security: Use sparingly. Prefer positive allowlists.
CEL (Common Expression Language)
Complex logic using CEL expressions for advanced authorization rules.
[!NOTE] Optional Feature (Rust): CEL support requires the
celfeature flag:tenuo = { version = "0.1.0-beta.1", features = ["cel"] }This reduces dependencies for users who don’t need CEL. Without the feature, CEL constraints can still be deserialized (for wire format interoperability), but evaluation returns
FeatureNotEnabled { feature: "cel" }.Python SDK always includes CEL support.
from tenuo import CEL
# Simple comparison
CEL("amount < 10000 && amount > 0")
# Multi-parameter validation
CEL("budget < revenue * 0.1 && currency == 'USD'")
How it works:
- CEL expressions evaluate to boolean (true/false)
- For object values, each field becomes a top-level variable
- For primitive values, the value is available as
value - Expressions are compiled once and cached for performance (max 1000 entries)
Example:
from tenuo import Warrant, CEL
# Budget must be less than 10% of revenue
warrant = (Warrant.mint_builder()
.capability("create_campaign",
budget_check=CEL("budget < revenue * 0.1 && budget > 0"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# When tool is called with:
# create_campaign(budget=5000, revenue=100000, ...)
# CEL evaluates: 5000 < 100000 * 0.1 && 5000 > 0 -> true (OK)
Standard Library Functions
Tenuo provides built-in functions for common use cases:
Time Functions:
# Check if timestamp hasn't expired
CEL("!time_is_expired(deadline)")
# Only allow if created within last hour
CEL("time_since(created_at) < 3600")
# Get current time (requires dummy arg due to library limitation)
CEL("time_now(null).startsWith('2024')")
| Function | Signature | Description |
|---|---|---|
time_now(unused) |
(_) -> String |
Returns current time in RFC3339 format |
time_is_expired(ts) |
(String) -> bool |
Checks if RFC3339 timestamp has passed |
time_since(ts) |
(String) -> i64 |
Seconds since RFC3339 timestamp (0 if invalid/future) |
Network Functions:
# Only allow requests from internal network
CEL("net_in_cidr(ip, '10.0.0.0/8') || net_in_cidr(ip, '192.168.0.0/16')")
# Block public IPs
CEL("net_is_private(source_ip)")
| Function | Signature | Description |
|---|---|---|
net_in_cidr(ip, cidr) |
(String, String) -> bool |
Check if IP (v4/v6) is in CIDR block |
net_is_private(ip) |
(String) -> bool |
Check if IP is in private range (RFC 1918) |
Time-bounded Example:
from tenuo import Warrant, CEL
# Only allow if order created within last 24 hours
warrant = (Warrant.mint_builder()
.capability("process_order",
freshness=CEL("time_since(created_at) < 86400"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
Network Example:
from tenuo import Warrant, CEL
# Only allow API calls from private network
warrant = (Warrant.mint_builder()
.capability("api_call",
network=CEL("net_in_cidr(source_ip, '10.0.0.0/8')"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
CEL Attenuation
Child CEL constraints are automatically combined with parent using AND logic:
from tenuo import Warrant, CEL
# Parent: budget < 10000
parent = (Warrant.mint_builder()
.capability("spend", budget_rule=CEL("budget < 10000"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# Child: Add additional constraint (auto-AND'd)
child = (parent.grant_builder()
.capability("spend", budget_rule=CEL("currency == 'USD'"))
.grant(key))
# Effective child expression: (budget < 10000) && (currency == 'USD')
Syntactic Monotonicity (Conservative Approach)
Tenuo enforces Syntactic Monotonicity for CEL, not Semantic Monotonicity.
Child expression must literally be (parent) && new_predicate. It cannot be a semantically equivalent but differently structured expression.
from tenuo import Warrant, CEL
# Parent CEL
parent = (Warrant.mint_builder()
.capability("api_call", network=CEL("net_in_cidr(ip, '10.0.0.0/8')"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# REJECTED: Semantically narrower but not syntactically derived
child = (parent.grant_builder()
.capability("api_call", network=CEL("net_in_cidr(ip, '10.1.0.0/16')")) # FAILS
.grant(key))
# Even though 10.1.0.0/16 is subset of 10.0.0.0/8, this is REJECTED
# ALLOWED: Syntactically derived (AND'd)
child = (parent.grant_builder()
.capability("api_call",
network=CEL("(net_in_cidr(ip, '10.0.0.0/8')) && net_in_cidr(ip, '10.1.0.0/16')"))
.grant(key))
# Now it's ALLOWED because it's (parent) && additional_check
Why Syntactic?
Semantic analysis (proving one expression is strictly narrower) requires:
- Automated theorem proving
- Understanding domain semantics (CIDR blocks, time logic, etc.)
- Potential false negatives or security holes
Syntactic monotonicity is conservative but secure: If the child is (parent) && X, it’s guaranteed to be narrower or equal.
Recommendation: Use simpler constraint types (Pattern, Range, OneOf) when possible. Reserve CEL for truly complex logic that can’t be expressed otherwise.
Security Properties
Sandboxed Execution: CEL cannot execute arbitrary code, only evaluate expressions
Deterministic: Same inputs always produce same results
Cached Programs: Compiled expressions cached (max 1000) for performance
Type Safe: Must return boolean or evaluation fails
No Side Effects: Expressions are pure - no I/O, no state mutation
Safe Standard Library: Only time/network parsing functions, no file/network I/O
Security Considerations
DoS Protection
While CEL expressions are sandboxed, extremely complex expressions could still consume CPU:
# Potentially expensive (though bounded by compilation)
CEL("(((((a && b) || (c && d)) && ((e || f) && (g || h))) || ...) ...")
Mitigations in place:
- Compilation fails on malformed expressions (syntax errors caught early)
- Cache limit (1000 entries) prevents unbounded memory growth
cel-interpreterv0.8.1 (no known DoS vulnerabilities)
Best Practices
- Keep expressions simple - prefer built-in constraint types when possible
- Test expressions before deployment with representative inputs
- Use syntactic attenuation - child must be
(parent) && Xfor safety
Important Notes
- CEL expressions must return boolean. Non-boolean results cause
CelError. - The constraint key (e.g.,
"budget_check") is informational; the expression defines the logic. - Syntactic monotonicity is enforced for attenuation (see above).
- Standard library functions are safe and deterministic (no I/O beyond time/IP parsing).
Constraint Narrowing (Attenuation)
When attenuating a warrant, child constraints must be contained within parent constraints.
Attenuation Compatibility Matrix
| Parent Type | Can Attenuate To |
|---|---|
Wildcard() |
Any constraint type (universal) |
Pattern() |
Pattern (if narrower), Exact (if matches) |
Regex() |
Same Regex only, Exact (if matches) |
Exact() |
Same Exact only |
OneOf() |
OneOf (subset), NotOneOf, Exact (if in set) |
NotOneOf() |
NotOneOf (more exclusions) |
Range() |
Range (narrower bounds), Exact (if in range) |
Cidr() |
Cidr (subnet), Exact (if IP in network) |
UrlPattern() |
UrlPattern (narrower), Exact (if matches) |
Contains() |
Contains (more required values) |
Subset() |
Subset (fewer allowed values) |
All() |
All (more constraints) |
AnyOf() |
AnyOf (fewer alternatives) |
CEL() |
CEL (conjunction with parent) |
Key Limitations:
- Regex: Cannot narrow to different regex patterns (undecidable subset problem)
- Exact: Cannot change value at all
- Range: If parent bound is exclusive, child cannot make it inclusive at the same value (would widen)
- No attenuation TO Wildcard: Would re-widen authority
- Not: Attenuation not supported (use positive constraints instead)
Cross-Type Containment
Some constraint types can contain different types during attenuation:
| Parent | Child | Containment Rule |
|---|---|---|
Wildcard() |
Any type | Universal parent - contains everything |
Pattern("*@co.com") |
Exact("cfo@co.com") |
Child matches parent glob |
Regex(r"^dev-.*") |
Exact("dev-web") |
Child matches parent regex |
Range(min=0, max=100) |
Exact("50") |
Child numeric value within range |
Cidr("10.0.0.0/8") |
Exact("10.1.2.3") |
Child IP within parent network |
Cidr("10.0.0.0/8") |
Cidr("10.1.0.0/16") |
Child is subnet of parent |
UrlPattern("https://*.example.com/*") |
Exact("https://api.example.com/v1") |
Child URL matches parent pattern |
UrlPattern("https://*.example.com/*") |
UrlPattern("https://api.example.com/v1/*") |
Child pattern is narrower |
OneOf(["a","b","c"]) |
Exact("b") |
Child value is in parent set |
OneOf(["a","b","c"]) |
NotOneOf(["c"]) |
Carves holes (allows a, b) |
Special Rules
| Rule | Description |
|---|---|
Wildcard parent |
Contains ANY child constraint type |
Wildcard child |
NEVER allowed (would widen permissions) |
Regex -> Regex |
Must be IDENTICAL pattern (subset undecidable) |
Range inclusivity |
Exclusive bounds cannot become inclusive at same value |
Limits
To ensure system stability and prevent denial-of-service attacks, the following hard limits are enforced:
- Max Constraint Depth: 32 levels. (e.g.
Not(Not(...))nested 32 times). - Max Constraint Size: Generally bounded by the 64KB Max Warrant Size.
For most use cases, depth 32 is more than sufficient. Generated policies from automated systems should respect this limit.
Examples:
# Wildcard -> Anything: Wildcard is the universal parent
parent = Wildcard()
child = Pattern("staging-*") # OK - Wildcard contains everything
child = Range(min=0, max=100) # OK - even different types
child = Wildcard() # OK - Wildcard contains Wildcard
# Nothing -> Wildcard: would expand permissions
parent = Pattern("*")
child = Wildcard() # FAILS - cannot widen to Wildcard
# Pattern -> Exact: exact value must match the pattern
parent = Pattern("*@company.com")
child = Exact("cfo@company.com") # OK - matches pattern
# Regex -> Exact: exact value must match the regex
parent = Regex(r"^dev-.*$")
child = Exact("dev-web") # OK - matches regex
child = Exact("production") # FAILS - doesn't match
# Regex -> Regex: must be identical (subset is undecidable)
parent = Regex(r"^staging-.*$")
child = Regex(r"^staging-.*$") # OK - identical
child = Regex(r"^staging-web$") # FAILS - even if semantically narrower
# Range -> Exact: numeric value must be within range
parent = Range(min=0, max=100)
child = Exact("50") # OK - 50 is in [0, 100]
child = Exact("150") # FAILS - 150 > 100
# OneOf -> Exact: exact value must be in the set
parent = OneOf(["read", "write", "delete"])
child = Exact("read") # OK - "read" is in set
# OneOf -> NotOneOf: carve holes from allowed set
parent = OneOf(["staging", "production", "dev"])
child = NotOneOf(["production"]) # OK - allows staging, dev only
# NotOneOf -> NotOneOf: must exclude MORE values
parent = NotOneOf(["admin"])
child = NotOneOf(["admin", "root"]) # OK - excludes more
# Contains -> Contains: must require MORE values
parent = Contains(["read"])
child = Contains(["read", "write"]) # OK - requires more
# Subset -> Subset: must allow FEWER values
parent = Subset(["a", "b", "c"])
child = Subset(["a", "b"]) # OK - allows fewer
Incompatible Cross-Types
Pattern->Range: String matching vs numeric boundsOneOf->Pattern: Set membership vs glob matching- Any type ->
Wildcard: Would expand permissions
Pattern Narrowing
from tenuo import Warrant, Pattern
# Parent: /data/*
parent = (Warrant.mint_builder()
.capability("read_file", path=Pattern("/data/*"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# Child: /data/reports/* (narrower) - OK
child = (parent.grant_builder()
.capability("read_file", path=Pattern("/data/reports/*"))
.grant(key))
# Child: /* (wider) - FAILS
child = (parent.grant_builder()
.capability("read_file", path=Pattern("/*"))
.grant(key)) # MonotonicityViolation
Range Narrowing
from tenuo import Warrant, Range
# Parent: max 15 replicas
parent = (Warrant.mint_builder()
.capability("scale", replicas=Range.max_value(15))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# Child: max 10 (narrower) - OK
child = (parent.grant_builder()
.capability("scale", replicas=Range.max_value(10))
.grant(key))
# Child: max 20 (wider) - FAILS
child = (parent.grant_builder()
.capability("scale", replicas=Range.max_value(20))
.grant(key)) # MonotonicityViolation
OneOf Narrowing
from tenuo import Warrant, OneOf
# Parent: ["a", "b", "c"]
parent = (Warrant.mint_builder()
.capability("action", type=OneOf(["a", "b", "c"]))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# Child: ["a", "b"] (subset) - OK
child = (parent.grant_builder()
.capability("action", type=OneOf(["a", "b"]))
.grant(key))
# Child: ["a", "b", "d"] (adds "d") - FAILS
child = (parent.grant_builder()
.capability("action", type=OneOf(["a", "b", "d"]))
.grant(key)) # MonotonicityViolation
Regex Narrowing
Regex constraints are conservative: Child regex must have identical pattern to parent.
from tenuo import Warrant, Regex, Exact
# Parent: regex pattern
parent = (Warrant.mint_builder()
.capability("query", env=Regex(r"^(staging|dev)-.*$"))
.holder(key.public_key)
.ttl(3600)
.mint(key))
# Cannot narrow to different regex (even if provably narrower) - FAILS
child = (parent.grant_builder()
.capability("query", env=Regex(r"^staging-.*$"))
.grant(key)) # MonotonicityViolation
# Can keep same pattern - OK
child = (parent.grant_builder()
.capability("query", env=Regex(r"^(staging|dev)-.*$"))
.grant(key))
# Can narrow to Exact (if it matches parent regex) - OK
child = (parent.grant_builder()
.capability("query", env=Exact("staging-web"))
.grant(key))
Why: Determining if one regex is a subset of another is undecidable in general. Tenuo takes a conservative approach for security.
Recommendation: Use Pattern() for simple matching that needs attenuation, or Exact() for specific values.
Using Constraints with Tools
With @guard Decorator
from tenuo import guard, Pattern, Range
@guard(tool="transfer_money")
def transfer_money(account: str, amount: float):
# Tenuo checks:
# - "account" against any Pattern/Exact constraint
# - "amount" against any Range constraint
...
With guard()
from tenuo.langchain import guard
# Protect tools with bound warrant
protected = guard([read_file, write_file, delete_file], bound)
Common Patterns
Wildcard to Specific Constraints
# Parent: any query
async with mint(Capability("search", query=Wildcard())):
# Child: narrow to pattern
async with grant(Capability("search", query=Pattern("*public*"))):
await search(query="public data") # OK
File Path Constraints
# Read-only access to reports directory
async with mint(Capability("read_file", path=Pattern("/data/reports/*"))):
await read_file(path="/data/reports/q3.csv") # OK
await read_file(path="/etc/passwd") # FAILS
Replica/Capacity Limits
# Limit replica counts
async with mint(Capability("scale", replicas=Range.max_value(15))):
await scale(replicas=5) # OK
await scale(replicas=20) # FAILS
Environment Restrictions
# Only staging and dev
async with mint(Capability("deploy", env=OneOf(["staging", "dev"]))):
await deploy(env="staging") # OK
await deploy(env="production") # FAILS
Scoped Database Access
# Only specific tables
async with mint(Capability("query", table=OneOf(["users", "orders"]))):
await query(table="users") # OK
await query(table="secrets") # FAILS
Pattern Best Practices
Pattern Uses Glob Syntax, Not Regex
Pattern uses glob syntax (like shell wildcards), not regular expressions:
| Syntax | Meaning | Example |
|---|---|---|
* |
Match any characters | staging-* → staging-web |
? |
Match single character | env-? → env-a |
[abc] |
Character class | [abc].txt → a.txt |
{a,b} |
Alternation | {dev,staging}-* → dev-web |
Common mistakes:
# ❌ WRONG: Pipe is not OR in glob
Pattern("weather *|news *") # Treats | as literal character
# ✅ CORRECT: Use curly braces for alternation
Pattern("{weather,news} *")
# ✅ CORRECT: Or use AnyOf() for complex cases
AnyOf([Pattern("weather *"), Pattern("news *")])
Prefer Explicit Over Permissive
# ⚠️ Too permissive - matches everything
Pattern("*")
# ✅ Better - explicit prefix
Pattern("staging-*")
# ✅ Best for known values - use Exact or OneOf
Exact("staging-web")
OneOf(["staging-web", "staging-db"])
Keep Patterns Simple
Attenuation validation works best with simple prefix/suffix patterns:
# ✅ Simple prefix - attenuation works reliably
Pattern("/data/*") # Parent
Pattern("/data/reports/*") # Child (narrower) ✓
# ⚠️ Complex patterns - attenuation may be conservative
Pattern("*-{prod,staging}-*") # Harder to validate containment
Use Exact/OneOf for High-Security Cases
When precision matters more than flexibility:
# For known, enumerable values
OneOf(["read", "write", "delete"])
# For exact matches
Exact("/etc/passwd") # Only this exact path
# For IP ranges
Cidr("10.0.0.0/8")
Defense in Depth: File Paths
Tenuo constraints validate the logical policy (does the pattern allow this path?). For file operations, you should also validate the physical path to prevent symlink attacks and traversal.
The One-Two Punch
use path_jail;
// Step 1: Tenuo validates policy
if warrant.allows("read_file", &args) {
// Step 2: path_jail validates filesystem reality
let safe_path = path_jail::join("/data", &args.path)?;
std::fs::read_to_string(safe_path)?
}
Why Both?
| Layer | What it catches | Example |
|---|---|---|
| Tenuo (Pattern) | Policy violations | path="/etc/passwd" blocked by Pattern("/data/*") |
| path_jail | Traversal attacks | path="/data/../etc/passwd" blocked after normalization |
| path_jail | Symlink escapes | path="/data/link" where link → /etc |
Recommended Pattern
from path_jail import Jail # pip install path_jail
jail = Jail("/data")
@guard(tool="read_file")
async def read_file(path: str) -> str:
# Tenuo already validated the constraint
# Now validate the actual filesystem path
safe_path = jail.join(path)
return safe_path.read_text()
Tenuo defines the rules. path_jail enforces them on the filesystem.
See: path_jail on PyPI
See Also
- 🔬 Explorer Playground — Test constraints interactively
- AI Agent Patterns — P-LLM/Q-LLM, prompt injection defense
- API Reference — Full constraint API
- Security — How constraints fit into the security model
- LangGraph Integration — Using constraints with LangGraph