use std::collections::BTreeMap;
use std::fmt;
pub use agent_registry_contract::ResourceRelations;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const SERVICE_NAME: &str = "agent-runtime-identity";
pub const AUDIENCE: &str = "agent-runtime-identity";
pub const DEFAULT_PORT: u16 = 5111;
pub const HEALTH_PATH: &str = "/health";
pub const IDENTITIES_PATH: &str = "/internal/v1/runtime-identities";
pub const LIST_IDENTITIES_PATH: &str = "/internal/v1/runtime-identities:list";
pub const IDENTITY_PATH: &str = "/internal/v1/runtime-identities/{identity_id}";
pub const ACTIVATE_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:activate";
pub const SUSPEND_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:suspend";
pub const RESUME_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:resume";
pub const REVOKE_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:revoke";
pub const PERMISSIONS_PATH: &str = "/internal/v1/runtime-identities/{identity_id}/permissions";
pub const EFFECTIVE_PERMISSIONS_PATH: &str =
"/internal/v1/runtime-identities/{identity_id}/effective-permissions";
pub const PERMISSION_CHECK_PATH: &str = "/internal/v1/runtime-permissions:check";
pub const ISSUE_CREDENTIAL_PATH: &str = "/internal/v1/runtime-identities/credentials:issue";
pub const VERIFY_CREDENTIAL_PATH: &str = "/internal/v1/runtime-credentials:verify";
pub const REVOKE_CREDENTIAL_PATH: &str = "/internal/v1/runtime-credentials:revoke";
pub const JWKS_PATH: &str = "/.well-known/runtime-identity-jwks.json";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TenantProjectScope {
pub tenant_id: String,
pub project_id: String,
}
impl TenantProjectScope {
pub fn new(
tenant_id: impl Into<String>,
project_id: impl Into<String>,
) -> Result<Self, String> {
let value = Self {
tenant_id: tenant_id.into(),
project_id: project_id.into(),
};
validate_identifier("tenant_id", &value.tenant_id, 128)?;
validate_identifier("project_id", &value.project_id, 128)?;
Ok(value)
}
pub fn local() -> Self {
Self {
tenant_id: "local".into(),
project_id: "default".into(),
}
}
}
macro_rules! identifier {
($name:ident, $label:literal, $max:expr) => {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
let value = value.into();
validate_identifier($label, &value, $max)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
};
}
identifier!(RuntimeIdentityId, "runtime_identity_id", 255);
identifier!(PermissionGrantId, "permission_grant_id", 255);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CredentialCapability(String);
impl CredentialCapability {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
let value = value.into();
if value.is_empty() || value.len() > 128 {
return Err("capability must contain 1..=128 bytes".into());
}
let segments = value.split(':').collect::<Vec<_>>();
if segments.len() != 3
|| segments.iter().any(|segment| {
segment.is_empty()
|| (*segment != "*"
&& !segment.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')
}))
})
{
return Err("capability must contain three valid colon-delimited segments".into());
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CredentialCapability {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeIdentityKind {
Code,
Evaluate,
Tool,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeIdentityStatus {
Pending,
Active,
Suspended,
Revoked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeIdentity {
pub id: RuntimeIdentityId,
#[serde(default)]
pub name: String,
pub scope: TenantProjectScope,
pub kind: RuntimeIdentityKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default)]
pub relations: ResourceRelations,
pub status: RuntimeIdentityStatus,
pub revision: u64,
pub grant_revision: u64,
pub credential_generation: u64,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRuntimeIdentityRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<RuntimeIdentityId>,
pub name: String,
pub kind: RuntimeIdentityKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default)]
pub relations: ResourceRelations,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub idempotency_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UpdateRuntimeIdentityRequest {
pub expected_revision: u64,
pub name: String,
#[serde(default)]
pub idempotency_key: String,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRuntimeIdentityResponse {
pub identity: RuntimeIdentity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bootstrap_token: Option<String>,
}
impl fmt::Debug for CreateRuntimeIdentityResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CreateRuntimeIdentityResponse")
.field("identity", &self.identity)
.field(
"bootstrap_token",
&self.bootstrap_token.as_ref().map(|_| "[REDACTED]"),
)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TransitionIdentityRequest {
pub expected_revision: u64,
pub idempotency_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListRuntimeIdentitiesRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<RuntimeIdentityStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<RuntimeIdentityId>,
#[serde(default = "default_page_size")]
pub limit: usize,
}
impl Default for ListRuntimeIdentitiesRequest {
fn default() -> Self {
Self {
status: None,
after: None,
limit: default_page_size(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListRuntimeIdentitiesResponse {
pub items: Vec<RuntimeIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next: Option<RuntimeIdentityId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PermissionGrantInput {
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PermissionGrant {
pub id: PermissionGrantId,
pub identity_id: RuntimeIdentityId,
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReplacePermissionsRequest {
pub expected_revision: u64,
pub grants: Vec<PermissionGrantInput>,
pub idempotency_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PermissionsResponse {
pub identity_id: RuntimeIdentityId,
pub revision: u64,
pub grants: Vec<PermissionGrant>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EffectivePermissionsResponse {
pub identity_id: RuntimeIdentityId,
pub audience: String,
pub grant_revision: u64,
pub capabilities: Vec<CredentialCapability>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CheckPermissionRequest {
pub identity_id: RuntimeIdentityId,
pub audience: String,
pub capability: CredentialCapability,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CheckPermissionResponse {
pub allowed: bool,
pub grant_revision: u64,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct IssueRuntimeCredentialRequest {
pub workload_proof: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proof_audience: Option<String>,
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
}
impl fmt::Debug for IssueRuntimeCredentialRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IssueRuntimeCredentialRequest")
.field("workload_proof", &"[REDACTED]")
.field("proof_audience", &self.proof_audience)
.field("audience", &self.audience)
.field("capabilities", &self.capabilities)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct IssueRuntimeCredentialResponse {
pub access_token: String,
pub token_type: String,
pub expires_in_seconds: u64,
pub key_id: String,
}
impl fmt::Debug for IssueRuntimeCredentialResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IssueRuntimeCredentialResponse")
.field("access_token", &"[REDACTED]")
.field("token_type", &self.token_type)
.field("expires_in_seconds", &self.expires_in_seconds)
.field("key_id", &self.key_id)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerifyRuntimeCredentialRequest {
pub access_token: String,
pub audience: String,
}
impl fmt::Debug for VerifyRuntimeCredentialRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VerifyRuntimeCredentialRequest")
.field("access_token", &"[REDACTED]")
.field("audience", &self.audience)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeCredentialClaims {
pub issuer: String,
pub runtime_identity_id: RuntimeIdentityId,
pub scope: TenantProjectScope,
#[serde(default)]
pub relations: ResourceRelations,
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
pub credential_generation: u64,
pub grant_revision: u64,
pub issued_at_seconds: u64,
pub expires_at_seconds: u64,
pub token_id: String,
pub key_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerifyRuntimeCredentialResponse {
pub active: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claims: Option<RuntimeCredentialClaims>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RevokeRuntimeCredentialRequest {
pub token_id: String,
pub expires_at_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeCredentialPublicKey {
pub kty: String,
#[serde(rename = "use")]
pub key_use: String,
pub alg: String,
pub kid: String,
pub crv: String,
pub x: String,
pub y: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeCredentialKeySet {
pub issuer: String,
pub keys: Vec<RuntimeCredentialPublicKey>,
pub cache_max_age_seconds: u64,
}
pub fn validate_audience(value: &str) -> Result<(), String> {
validate_identifier("audience", value, 128)
}
pub fn validate_runtime_name(value: &str) -> Result<String, String> {
let value = value.trim();
if value.is_empty() || value.chars().count() > 128 {
return Err("runtime name must contain 1..=128 characters".into());
}
if value.chars().any(char::is_control) {
return Err("runtime name must not contain control characters".into());
}
Ok(value.to_string())
}
pub fn validate_capabilities(values: &[CredentialCapability]) -> Result<(), String> {
if values.is_empty() || values.len() > 32 {
return Err("capabilities must contain 1..=32 entries".into());
}
let mut unique = std::collections::BTreeSet::new();
if !values.iter().all(|value| unique.insert(value.as_str())) {
return Err("capabilities must not contain duplicates".into());
}
Ok(())
}
fn validate_identifier(label: &str, value: &str, max_bytes: usize) -> Result<(), String> {
if value.is_empty() || value.len() > max_bytes {
return Err(format!("{label} must contain 1..={max_bytes} bytes"));
}
if !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'.' | b'-' | b'_' | b'@')
}) {
return Err(format!("{label} contains unsupported characters"));
}
Ok(())
}
fn default_page_size() -> usize {
50
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_bodies_cannot_override_scope() {
let result = serde_json::from_value::<CreateRuntimeIdentityRequest>(serde_json::json!({
"name": "runtime-test",
"kind": "code",
"tenantId": "attacker"
}));
assert!(result.is_err());
}
#[test]
fn runtime_name_is_trimmed_and_bounded() {
assert_eq!(validate_runtime_name(" code-dev ").unwrap(), "code-dev");
assert!(validate_runtime_name(" ").is_err());
assert!(validate_runtime_name("runtime\nname").is_err());
assert!(validate_runtime_name(&"r".repeat(129)).is_err());
}
#[test]
fn credentials_are_redacted_from_debug() {
let request = IssueRuntimeCredentialRequest {
workload_proof: "sensitive-bootstrap-value".into(),
proof_audience: None,
audience: "agent-model-infra".into(),
capabilities: vec![CredentialCapability::new("model-profile:echo:invoke").unwrap()],
};
assert!(!format!("{request:?}").contains("sensitive-bootstrap-value"));
}
#[test]
fn capabilities_are_bounded_and_unique() {
let capability = CredentialCapability::new("model-profile:echo:invoke").unwrap();
assert!(validate_capabilities(std::slice::from_ref(&capability)).is_ok());
assert!(validate_capabilities(&[capability.clone(), capability]).is_err());
assert!(CredentialCapability::new("model-profile:*:invoke").is_ok());
assert!(CredentialCapability::new("model-profile:echo:*").is_ok());
assert!(CredentialCapability::new("model-profile:echo:inv*oke").is_err());
}
}