Quint uses a 6-level token hierarchy where each level derives from its parent. Tokens are prefixed for O(1) type detection and contain ES256-signed JWT claims.
Hierarchy Diagram
┌───────────┐
│ App Token │ qt_app_* (management plane)
│ 1 year │
└─────┬─────┘
│
┌─────▼─────┐
│ Bearer │ qt_bearer_* (data plane root)
│ 90 days │
└─────┬─────┘
│
┌─────▼─────┐
┌─────│ Agent │ qt_agent_* (per-agent identity)
│ │ 24 hours │
│ └─────┬─────┘
│ │
┌─────▼─────┐ ┌─▼──────────┐
│ Session │ │ Subagent │ qt_subagent_*
│ 1 hour │ │ 4 hours │
└────────────┘ └─────┬──────┘
│
┌─────▼─────┐
│ Session │ qt_session_*
│ 1 hour │
└────────────┘
┌────────────┐
│ Override │ qt_override_* (ephemeral, single-use)
│ 5 minutes │
└────────────┘
Token Types
1. App Token (qt_app_*)
Purpose: Management-plane operations — creating other tokens, rotating keys, registering webhooks.
| Field | Value |
|---|
| Prefix | qt_app_ |
| Default TTL | 1 year |
| Auth required | Bootstrap / admin |
| JWT claims | jti, sub (customer_id), typ: "app", iat, exp |
| Storage | Hash stored in auth.app_tokens |
Create:
POST /tokens/app
{
"customer_id": "uuid",
"name": "Production API",
"scopes": ["*"]
}
App tokens are the root of the derivation chain. They have scopes but no RBAC policy — management tokens bypass RBAC.
2. Bearer Token (qt_bearer_*)
Purpose: Data-plane root token — the entry point for agent authentication. Environment-scoped.
| Field | Value |
|---|
| Prefix | qt_bearer_ |
| Default TTL | 90 days |
| Auth required | App token |
| JWT claims | jti, sub, typ: "bearer", parent_jti (app token), env |
| Storage | auth.bearer_tokens with FK to app_tokens |
Create:
POST /tokens/bearer
{
"customer_id": "uuid",
"app_token_hash": "sha256-hash-of-app-token",
"environment": "production"
}
Environment values: development, staging, production
3. Agent Token (qt_agent_*)
Purpose: Per-agent identity with RBAC policy. This is the primary token used in Authorization: Bearer headers when submitting events.
| Field | Value |
|---|
| Prefix | qt_agent_ |
| Default TTL | 24 hours |
| Auth required | Bearer token |
| JWT claims | jti, sub, typ: "agent", parent_jti (bearer), agent_id, rbac |
| Storage | auth.agent_tokens with FK to bearer_tokens |
Create:
POST /tokens/agent
{
"customer_id": "uuid",
"bearer_jti": "jti-of-bearer-token",
"agent_id": "code-review-agent",
"agent_name": "Code Review Agent",
"rbac": {
"allowed_actions": ["data:read:*", "code:review:*"],
"denied_actions": ["data:write:*"],
"allowed_resources": ["repo:*"],
"denied_resources": [],
"max_sensitivity_level": 3
}
}
RBAC enforcement: On every event submission, the agent’s RBAC policy is checked against event.action and event.target_resource.
4. Subagent Token (qt_subagent_*)
Purpose: Delegated token for sub-agents with permission narrowing. A sub-agent cannot have broader permissions than its parent agent.
| Field | Value |
|---|
| Prefix | qt_subagent_ |
| Default TTL | 4 hours |
| Auth required | Agent token |
| JWT claims | jti, sub, typ: "subagent", parent_jti (agent), agent_id, rbac, depth |
| Storage | auth.subagent_tokens with FK to agent_tokens |
Permission narrowing rules:
child.allowed_actions must be a subset of parent.allowed_actions
child.denied_actions must be a superset of parent.denied_actions
child.allowed_resources must be a subset of parent.allowed_resources
child.denied_resources must be a superset of parent.denied_resources
child.max_sensitivity_level must be <= parent.max_sensitivity_level
Delegation depth: Configurable per customer (default max: 3). Depth is tracked in the depth claim and incremented on each delegation.
5. Session Token (qt_session_*)
Purpose: Short-lived token binding an agent to a specific session with event counting.
| Field | Value |
|---|
| Prefix | qt_session_ |
| Default TTL | 1 hour |
| Auth required | Agent or subagent token |
| JWT claims | jti, sub, typ: "session", parent_jti, session_id |
| Storage | auth.session_tokens |
| Counter | Redis INCR on quint:auth:session_events:{"{jti}"} |
Create:
POST /tokens/session
{
"customer_id": "uuid",
"parent_jti": "jti-of-agent-or-subagent",
"parent_type": "agent",
"session_id": "session-2024-01-15-abc",
"max_events": 1000
}
Event counting: Each event submitted with a session token increments a Redis counter. When event_count exceeds max_events, the API returns SessionExhaustedError.
Usage: Sent via X-Quint-Session header alongside the Authorization: Bearer qt_agent_* header.
6. Override Token (qt_override_*)
Purpose: Ephemeral, single-use token for human override decisions on held high-risk events.
| Field | Value |
|---|
| Prefix | qt_override_ |
| Default TTL | 5 minutes |
| Auth required | Internal (API proxy) |
| JWT claims | jti, sub, typ: "override", event_id, allowed_decisions |
| Storage | auth.override_tokens with use_count/max_uses |
Flow:
- Scoring pipeline flags event as high-risk, puts it on hold
- System creates override token linked to the event
- Human reviewer receives token (via webhook/notification)
- Human submits decision via
POST /overrides/{"{event_id}"}/decide with override token
- Decision is cryptographically attested with HMAC co-signature
Derivation Chain Verification
On every token validation, the _verify_chain() method checks that the JWT claims are consistent with the token type:
| Token Type | Required Claims |
|---|
| App | (none — root of chain) |
| Bearer | parent_jti (app), env |
| Agent | parent_jti (bearer), agent_id, rbac |
| Subagent | parent_jti (agent), agent_id, rbac, depth |
| Session | parent_jti (agent/subagent), session_id |
| Override | event_id |
If any required claim is missing, validation fails with TokenInvalidError.
Token Prefixes
All tokens are prefixed for instant type detection without decoding:
TOKEN_PREFIX_APP = "qt_app_"
TOKEN_PREFIX_BEARER = "qt_bearer_"
TOKEN_PREFIX_AGENT = "qt_agent_"
TOKEN_PREFIX_SUBAGENT = "qt_subagent_"
TOKEN_PREFIX_SESSION = "qt_session_"
TOKEN_PREFIX_OVERRIDE = "qt_override_"
A raw token looks like: qt_agent_eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...