Skip to main content

systemprompt_models/auth/
actor.rs

1//! The actor of a request, derived from its credential alone.
2//!
3//! A bearer token of any kind is an agent acting for its user. The agent's
4//! name comes from signed claims, never from request metadata: the token's
5//! `purpose` (an agent run's `farm:<run>`), else its `client_id`, else
6//! [`UNNAMED_AGENT`]; a name that is not a valid [`AgentName`] (over 64 bytes,
7//! or a character outside `[A-Za-z0-9-_.:/]`) falls back the same way.
8//! `attended` is `true` only for a token whose `interaction` claim is
9//! `interactive` and that carries no `purpose`.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{AgentName, ClientId};
16
17use super::claims::TokenInteraction;
18
19pub const UNNAMED_AGENT: &str = "agent";
20
21/// Who a request acts as: the person, or an agent acting for them.
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(tag = "kind", rename_all = "snake_case")]
24pub enum RequestActor {
25    Human,
26    Agent { name: AgentName, attended: bool },
27}
28
29impl RequestActor {
30    #[must_use]
31    pub const fn is_agent(&self) -> bool {
32        matches!(self, Self::Agent { .. })
33    }
34
35    #[must_use]
36    pub const fn is_unattended_agent(&self) -> bool {
37        matches!(
38            self,
39            Self::Agent {
40                attended: false,
41                ..
42            }
43        )
44    }
45}
46
47#[must_use]
48pub fn bearer_actor(
49    purpose: Option<&str>,
50    client_id: Option<&ClientId>,
51    interaction: Option<TokenInteraction>,
52) -> RequestActor {
53    let attended = interaction == Some(TokenInteraction::Interactive) && purpose.is_none();
54    RequestActor::Agent {
55        name: agent_name(purpose, client_id),
56        attended,
57    }
58}
59
60#[must_use]
61pub fn agent_name(purpose: Option<&str>, client_id: Option<&ClientId>) -> AgentName {
62    purpose
63        .or_else(|| client_id.map(ClientId::as_str))
64        .and_then(|name| AgentName::try_new(name).ok())
65        .unwrap_or_else(|| AgentName::new(UNNAMED_AGENT))
66}