Debugging Guide
When authorization fails, Tenuo provides tools to understand exactly what went wrong.
Quick Debugging
1. Use why_denied()
Get a structured explanation of authorization failures:
result = warrant.why_denied("read_file", {"path": "/etc/passwd"})
if result.denied:
print(f"Denied: {result.deny_code}")
print(f"Field: {result.field}")
print(f"Suggestion: {result.suggestion}")
Output:
Denied: CONSTRAINT_VIOLATION
Field: path
Suggestion: Request a path matching Pattern("/data/*")
2. Use diagnose()
Get a complete warrant inspection:
from tenuo import diagnose
diagnose(warrant)
Output:
=== Warrant Inspection ===
ID: tnu_wrt_019b482c2a0473e3aa5ccf39e70e197c
Issuer: ed25519:abc123...
Holder: ed25519:def456...
Expires At: 2025-12-22T15:00:00Z
TTL Remaining: 0:45:30
Is Terminal: False
Is Expired: False
Tools: ['search', 'read_file']
Capabilities:
search:
query: Pattern("*")
read_file:
path: Pattern("/data/*")
3. Use explain() for Errors
Print actionable diagnostics from any Tenuo exception:
from tenuo import explain, explain_str
try:
protected_function()
except TenuoError as e:
explain(e) # Prints to stderr with suggested fixes
Output:
❌ CONSTRAINT_VIOLATION: Argument 'path' violated constraint
Constraint: Pattern("/data/*")
Value: /etc/passwd
💡 How to fix:
- Request a path matching the pattern "/data/*"
- Example: /data/reports/q3.csv
Get as string instead:
message = explain_str(e) # Returns string instead of printing
4. Use warrant.explain()
Get a human-readable explanation of any warrant:
# Basic explanation
print(warrant.explain())
# Include delegation chain
print(warrant.explain(include_chain=True))
Output:
Warrant tnu_wrt_019b482c...
Issuer: ed25519:abc123...
Holder: ed25519:def456...
Tools: ['search', 'read_file']
TTL: 45m remaining
Depth: 2
Chain:
[0] Root (control_plane) → issued to orchestrator
[1] orchestrator → delegated to worker
[2] worker (current)
5. Use info()
Get environment and configuration info for debugging:
from tenuo import info
print(info())
Output:
Tenuo Configuration
==================================================
[OK] SDK Version: 0.1.0b5
[OK] Rust Core: loaded (wire version 1)
[OK] Issuer Key: configured
CLI Debugging
Use the CLI to decode and inspect warrants:
# Decode a warrant
tenuo decode "eyJ0eXAi..."
# Validate authorization
tenuo validate "eyJ0eXAi..." --tool read_file --args '{"path": "/data/file.txt"}'
# Check configuration
tenuo version
See CLI Reference for full command documentation.
Explorer Playground
🔬 Tenuo Explorer — Interactive warrant debugging:
- Decode any warrant visually
- Test authorization with different arguments
- Inspect delegation chains
- Generate code snippets (Python, Rust)
- Build warrants with the visual builder
Warrants contain only signed claims, not secrets. They’re safe to paste and share.
Common Issues
Tool Not Authorized
Error: Tool 'delete_file' is not authorized
Cause: The warrant doesn’t include this tool in its allowed tools list.
Debug:
print(f"Warrant tools: {warrant.tools}")
# ['search', 'read_file'] # delete_file not listed
Fix: Request a warrant that includes the tool, or use a different tool.
Constraint Violation
Error: Constraint 'path' violated
Cause: The argument doesn’t match the warrant’s constraint.
Debug:
result = warrant.why_denied("read_file", {"path": "/etc/passwd"})
print(f"Field: {result.field}")
print(f"Constraint: {result.constraint}")
print(f"Value: {result.value}")
Output:
Field: path
Constraint: Pattern("/data/*")
Value: /etc/passwd
Fix: Request within the allowed constraints.
Unknown Field Rejected
Error: unknown field not allowed (zero-trust mode)
Cause: You defined at least one constraint, which activates closed-world mode. All arguments must be explicitly constrained.
Debug:
result = warrant.why_denied("api_call", {"url": "...", "timeout": 30})
print(f"Field: {result.field}")
# Field: timeout (unknown)
Fix options:
- Add a constraint for the field:
timeout=Range.max_value(60) - Use
Wildcard()to allow any value:timeout=Wildcard() - Opt out of closed-world:
_allow_unknown=True
See Closed-World Mode for details.
Warrant Expired
Error: Warrant has expired
Debug:
print(f"Expired: {warrant.is_expired}")
print(f"Expires at: {warrant.expires_at}")
print(f"TTL remaining: {warrant.ttl_remaining}")
Fix: Request a fresh warrant from the issuer.
Missing Context
Error: No warrant in context
Cause: @guard decorator expects a warrant in context, but none was set.
Fix:
from tenuo import warrant_scope, key_scope
# Wrap your call
with warrant_scope(warrant), key_scope(keypair):
protected_function()
Or use explicit BoundWarrant:
protected = guard(tools, warrant.bind(key))
Key Mismatch
Error: PoP signature invalid
Cause: The keypair used to sign doesn’t match the warrant’s holder.
Debug:
print(f"Warrant holder: {warrant.holder}")
print(f"Key public: {keypair.public_key}")
# Compare - should be equal
Fix: Use the correct keypair.
FastAPI/A2A: Authorization Denied (Remote PoP)
Error: Authorization denied (invalid PoP or constraint violation)
Cause: In remote verification (FastAPI, A2A), the server verifies a precomputed signature from the client. This generic error can mean:
- Wrong signing key - Client signed with different key than warrant holder
- Constraint violation - Arguments don’t satisfy warrant constraints
- Tool not in warrant - Requested tool not granted
Debug (Client side):
# Verify you're using the correct key
print(f"Warrant holder: {warrant.holder}")
print(f"Signing with: {my_key.public_key}")
assert warrant.holder == my_key.public_key
# Test locally before sending
result = warrant.why_denied("tool_name", {"arg": "value"})
if result.denied:
print(f"Would be denied: {result.deny_code}")
Debug (Server side):
# Enable debug logging
import logging
logging.getLogger("tenuo.fastapi").setLevel(logging.DEBUG)
logging.getLogger("tenuo.a2a").setLevel(logging.DEBUG)
Note: The error message is intentionally generic for security. Detailed reason is logged server-side but not returned to clients (prevents information disclosure).
FastAPI: Missing Headers
Error: 401 Unauthorized: Missing X-Tenuo-Warrant header
Cause: Client didn’t send required authorization headers.
Fix: Include both headers in request:
import httpx
response = httpx.get(
"https://api.example.com/search",
headers={
"Authorization": f"TenuoWarrant {warrant.to_base64()}",
"X-Tenuo-Pop": base64.urlsafe_b64encode(pop_signature).decode(),
}
)
Convenience Properties
Quick status checks:
# Time until expiry
warrant.ttl_remaining # timedelta(seconds=3600)
warrant.ttl # alias
# Status flags
warrant.is_expired # bool
warrant.is_terminal # bool - can't delegate further
# Human-readable constraints
warrant.capabilities # dict
# {'search': {'query': 'Pattern("*")'}, 'read_file': {'path': 'Pattern("/data/*")'}}
# Expiration time
warrant.expires_at # "2025-12-22T15:00:00Z"
Logic Checks (UX Only)
⚠️ These are NOT security checks - use for UI hints only:
# Check if tool is in warrant
if warrant.allows("search"):
show_search_button()
# Check if args would pass constraints
if warrant.allows("read_file", args={"path": "/data/file.txt"}):
show_file_picker()
Never use for authorization decisions:
# ❌ WRONG
if warrant.allows("delete"):
delete_database() # No PoP verification!
# ✅ CORRECT
if bound.validate("delete", {"id": "123"}):
delete_database()
Error Message Reference
| Error | Meaning | Common Fix |
|---|---|---|
Tool 'X' not authorized |
Tool not in warrant | Add tool to warrant |
Constraint 'X' violated |
Arg doesn’t match constraint | Use allowed values |
unknown field not allowed |
Closed-world mode active | Add constraint or _allow_unknown |
Warrant expired |
TTL exceeded | Get fresh warrant |
No warrant in context |
Missing context | Use warrant_scope() |
PoP signature invalid |
Wrong key | Use holder’s keypair |
MonotonicityViolation |
Tried to expand scope | Scopes only shrink |
Key 'X' not found |
Key not in registry | Register key or load_tenuo_keys() |
Logging
Enable debug logging for detailed traces:
import logging
# Tenuo components
logging.getLogger("tenuo").setLevel(logging.DEBUG)
logging.getLogger("tenuo.fastapi").setLevel(logging.DEBUG)
logging.getLogger("tenuo.langgraph").setLevel(logging.DEBUG)
Request IDs
Authorization errors include a request ID for log correlation:
# Client sees:
# "Authorization denied (ref: abc123)"
# Logs show:
# [abc123] Tool 'search' denied: query=/etc/passwd, expected=Pattern(/data/*)
@guard(tool="search")
def search(query: str):
...
Use this ID to find detailed logs on the server side.
See Also
- Security — Threat model, best practices
- API Reference — Full Python API documentation
- Constraints — Constraint types and behavior