Aleph
Concepts

Identity Context

Immutable identity snapshot flowing through the execution chain, enabling permission enforcement at the tool level.

Overview

IdentityContext is an immutable identity snapshot that flows through the entire execution chain, enabling identity-based permission enforcement at the tool execution level.

Every request that enters Aleph carries an identity -- whether from the owner, a guest with limited scope, or an unauthenticated caller. This identity is captured once at session creation and becomes read-only for the remainder of the request lifecycle. By making the identity immutable, the system prevents privilege escalation attacks where a caller might attempt to change roles mid-execution.

Identity Context Flow

1. Session Creation
   SessionManager
   ┌────────────────────────────────────────────┐
   │ Owner Session:                              │
   │   metadata = SessionIdentityMeta {          │
   │     role: Role::Owner,                      │
   │     identity_id: "owner",                   │
   │     scope: None,                            │
   │     source_channel: "gateway",              │
   │   }                                         │
   │                                             │
   │ Guest Session:                              │
   │   metadata = SessionIdentityMeta {          │
   │     role: Role::Guest,                      │
   │     identity_id: "<guest-uuid>",            │
   │     scope: Some(GuestScope { ... }),        │
   │     source_channel: "telegram",             │
   │   }                                         │
   └────────────────────────────────────────────┘


2. Resolved into IdentityContext
   IdentityContext::owner(session_key, source_channel)
   IdentityContext::guest(session_key, guest_id, scope, source_channel)
   IdentityContext::anonymous(session_key, source_channel)


3. Harness Execution (Orchestrator::dispatch)
   FlowRequest { agent_id, input, channel, ... }
   HarnessRunner::run(session_key, spec, input, sandbox, events, ...)


4. Tool Execution (ScopedToolService)
   ┌────────────────────────────────────────────┐
   │ let svc = ScopedToolService::new(           │
   │     registry,                               │
   │     allowed_tools,                          │
   │ );                                          │
   │                                             │
   │ match identity.role {                       │
   │     Role::Owner => full tool set visible,   │
   │     Role::Guest => scope.allows_tool(name), │
   │     Role::Anonymous => Denied,              │
   │ }                                           │
   └────────────────────────────────────────────┘

Key Components

ComponentLocationResponsibility
IdentityContext / GuestScope / Roleshared/protocol/src/auth.rs (aleph_protocol)Immutable identity snapshot, guest scope, role enum
SessionIdentityMetasrc/gateway/session_manager/mod.rsPersistent session identity metadata
AgentKeystore / AgentLedger / LedgerRecord / LedgerAction / ChainReportsrc/identity/{keystore,ledger,record,verify}.rsPer-agent Ed25519 key + hash-chained ledger
as_actor / current_actorsrc/identity/actor.rsDelegated-role task_local for sub-agent / delegation attribution
SessionKey / DmScopesrc/routing/session_key.rsHierarchical session keys and DM isolation strategy
Orchestrator / HarnessRunnersrc/orchestrator/Session-key resolution, dispatch, FlowRequest assembly
ScopedToolServicesrc/gateway/execution_engine/Tool-execution-layer allowed_tools enforcement

Role Types

Aleph defines three role tiers (in shared/protocol/src/auth.rs) that determine what a caller is allowed to do:

RolePermissionsUse Case
OwnerSkip tool filteringServer administrator
GuestConstrained by allowed_toolsTemporary access via invitation
AnonymousDenied by defaultUnauthenticated requests; the #[default] of Role

Owner

The owner role is assigned to the server administrator. IdentityContext::owner fills role = Role::Owner, identity_id = "owner", scope = None at construction; the execution side skips allowed_tools filtering, so ScopedToolService forwards the full visible tool set.

Guest

Guest sessions are created through the invitation system. A guest receives a bounded GuestScope defining which tools they can invoke and when access expires. The guest tool path goes through scope.allows_tool(name) (the GuestScope::allows_tool method), which is an exact-match == comparison -- no wildcards or category prefixes.

Anonymous

Anonymous requests follow IdentityContext::anonymous; the Role derive's #[default] picks the same path when no role is supplied. They are denied at the execution layer unless the call chain is running with authentication disabled in development mode.

GuestScope

When a guest session is created, SessionIdentityMeta carries a GuestScope (in shared/protocol/src/auth.rs):

pub struct GuestScope {
    pub allowed_tools: Vec<String>,   // Tool names (exact match)
    pub expires_at: Option<i64>,      // Unix seconds; None = no expiry
    pub display_name: Option<String>, // Human-readable name for audit logs
}
FieldTypeDescription
allowed_toolsVec<String>Exact-match tool names; the executor compares with == per element
expires_atOption<i64>Unix timestamp; GuestScope::is_expired(now) triggers at >=
display_nameOption<String>Friendly name shown in audit logs

Guest scope is enforced at the ScopedToolService tool-execution layer, not at the API boundary. A guest can send any request, but any tool call not listed in allowed_tools is dropped at the execution layer.

IdentityContext

IdentityContext embodies the "Certificate of Authority" pattern: permissions are frozen at construction, the value is immutable for its entire lifetime, all audit information is embedded, and no external queries are needed for independent verification.

pub struct IdentityContext {
    pub request_id: String,      // crate::ids::next_id()
    pub session_key: String,
    pub role: Role,
    pub identity_id: String,     // Owner="owner" | Guest=<uuid> | Anonymous="anonymous"
    pub scope: Option<GuestScope>,
    pub created_at: i64,         // SystemTime::now().duration_since(UNIX_EPOCH).as_secs()
    pub source_channel: String,
}

Factory methods:

  • IdentityContext::owner(session_key, source_channel)
  • IdentityContext::guest(session_key, guest_id, scope, source_channel)
  • IdentityContext::anonymous(session_key, source_channel)

Each factory self-fills request_id and created_at, so two contexts built at the same call site never share a request_id.

Identity Ledger and Signing

src/identity/ provides the "who acted" + "what happened" ledger pair:

  • AgentKeystore (src/identity/keystore.rs) -- per-agent Ed25519 keypair; public half and fingerprint live in security.db, private half lives in the shared Vault. Rotation retires a key rather than replacing it, so records signed under the old key remain verifiable.
  • AgentLedger (src/identity/ledger.rs) -- per-agent hash-chained ledger; every tool call (the only choke-point is tools::scoped::dispatch) appends one signed record.
  • verify_chain (src/identity/verify.rs) -- returns ChainReport / ChainFault to confirm chain integrity.
  • as_actor / current_actor (src/identity/actor.rs) -- a tokio::task_local that lets a delegated role file its own records rather than the parent's.

The ledger is not an impersonation defence: agent_id still arrives as a caller-supplied string on chat.send and is checked only for existence (crate::gateway::router). The ledger faithfully records the identity it was given (see docs/reference/AGENT_IDENTITY.md).

Example: Owner Request

use aleph_protocol::{IdentityContext, Role};
use std::time::{SystemTime, UNIX_EPOCH};

let session_key = String::from("agent:main:main");
let metadata = SessionIdentityMeta::owner("gateway");

let identity = metadata.to_identity_context(session_key);
// identity.role == Role::Owner
// identity.identity_id == "owner"
// identity.scope == None

Example: Guest Request with Invitation

use aleph_protocol::{GuestScope, IdentityContext, Role};

let scope = GuestScope {
    allowed_tools: vec!["translate".to_string()],
    expires_at: Some(
        SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + 3600
    ),
    display_name: Some("Alice".to_string()),
};

let session_key = String::from("agent:main:telegram:dm:<alice-uuid>");
let metadata = SessionIdentityMeta::guest(
    "<alice-uuid>".to_string(),
    scope,
    "telegram".to_string(),
);

let identity = metadata.to_identity_context(session_key);
// identity.role == Role::Guest
// identity.scope.allows_tool("translate")   == true
// identity.scope.allows_tool("bash_exec")    == false

See Also


26.7.x Addendum

Delegated Role Chain

26.7.26+: the as_actor task-local gives every delegated role on a delegation chain its own signing identity:

  • The identity chain is closed within the chain's lifecycle -- once a delegation is established, all signing happens within the chain
  • Sub-agents are no longer filed on the parent ledger; AgentKeystore gives each agent role its own key, and the private half never leaves the node
  • verify_chain replays LedgerRecord entries against the chain the signer belongs to

Fail-Closed

26.7.21 hardening: authorization fails closed when a policy file is missing, unreadable, or invalid. The [sandbox.command_policy] hard floor has the highest priority; no exec_tier can lower it.

Real Client IP

26.7.17+: the [gateway] trusted_proxy list decides whether the gateway reads the real client IP. Capacity limits, rate limits, audit, and connect-auth all depend on this. When misconfigured, all of these metrics use the proxy's IP.

Device Tickets and Token Rotation

26.7.17+ remote-auth on the gateway side:

  • gateway.ticket.create issues device tickets
  • gateway.token.current / gateway.token.rotate manage tokens
  • gateway.devices.list / gateway.devices.revoke manage the device list
  • Remote sessions are closed on token rotation; loopback sessions are preserved

TOFU Trust

26.7.18+ client-side Trust-on-First-Use for self-signed certificates:

  • Shared decision core + pinned TOFU trust store
  • SHA-256 fingerprint + SAN/Subject parsing
  • Approval splash page (fingerprint + SAN + TOFU/change warning)
  • Pending-cert state with approve/reject paths

On this page