use serde::{Deserialize, Serialize};
use crate::Principal;
#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
enum ScopePrincipal {
Reserved { module_id: String },
Direct {},
Unverified {},
}
impl From<ScopePrincipal> for Principal {
fn from(value: ScopePrincipal) -> Self {
match value {
ScopePrincipal::Reserved { module_id } => Self::Reserved { module_id },
ScopePrincipal::Direct {} => Self::Direct,
ScopePrincipal::Unverified {} => Self::Unverified,
}
}
}
fn deserialize_scope_principal<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Principal, D::Error> {
ScopePrincipal::deserialize(deserializer).map(Into::into)
}
fn deserialize_scope_principals<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<Principal>, D::Error> {
Vec::<ScopePrincipal>::deserialize(deserializer)
.map(|principals| principals.into_iter().map(Into::into).collect())
}
pub const CAP_SCOPES_V1: &str = "scopes/v1";
pub const CAP_ROUTE_ROLE_VERSIONS_V1: &str = "route-role-versions/v1";
pub const SCOPE_SYNC_OP: &str = "scope.sync";
pub const SCOPE_DESCRIBE_OP: &str = "scope.describe";
pub const MAX_LIVE_SCOPES_PER_OWNER: usize = 10_000;
pub const MAX_SCOPE_ATTRIBUTE_BYTES: usize = 4 * 1024;
pub const MAX_SCOPE_TOMBSTONES_PER_OWNER: usize = 1_000;
pub const MAX_CARRIER_TARGETS: usize = 16;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ScopeKind {
Head,
Worker,
Ephemeral,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ScopeParent {
#[serde(deserialize_with = "deserialize_scope_principal")]
pub owner: Principal,
#[serde(rename = "ref")]
pub scope_ref: String,
pub scope_epoch: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ScopeCarrier {
#[serde(deserialize_with = "deserialize_scope_principal")]
pub principal: Principal,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub targets: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ScopeAttributes {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub delegates: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flow_id: Option<String>,
}
impl ScopeAttributes {
pub fn is_empty(&self) -> bool {
self.agent_id.is_none() && !self.delegates && self.flow_id.is_none()
}
}
pub fn validate_flow_id(flow_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
crate::tool_call::validate_opaque_field("flow_id", flow_id)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ScopeRecord {
#[serde(rename = "ref")]
pub scope_ref: String,
pub scope_epoch: u64,
pub kind: ScopeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<ScopeParent>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "deserialize_scope_principals"
)]
pub child_owners: Vec<Principal>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub carriers: Vec<ScopeCarrier>,
#[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
pub attributes: ScopeAttributes,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ScopeSelector {
#[serde(deserialize_with = "deserialize_scope_principal")]
pub owner: Principal,
#[serde(rename = "ref")]
pub scope_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope_epoch: Option<u64>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ParentState {
Linked,
Pending,
Ended,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScopeRecordOutcome {
Created,
Replaced,
Updated,
Unchanged,
Refused,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScopeRecordResult {
#[serde(rename = "ref")]
pub scope_ref: String,
pub scope_epoch: u64,
pub outcome: ScopeRecordOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_state: Option<ParentState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScopeEnded {
#[serde(rename = "ref")]
pub scope_ref: String,
pub scope_epoch: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScopeStatus {
Live,
Ended,
NotLive,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScopeStamp {
pub owner: Principal,
#[serde(rename = "ref")]
pub scope_ref: String,
pub scope_epoch: u64,
pub kind: ScopeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<ScopeParent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_state: Option<ParentState>,
#[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
pub attributes: ScopeAttributes,
pub owner_authorized: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tool_call::OpaqueFieldError;
#[test]
fn flow_id_uses_the_shared_opaque_token_bounds_and_names_its_field() {
let field = "flow_id";
assert_eq!(validate_flow_id(""), Err(OpaqueFieldError::Empty { field }));
assert_eq!(validate_flow_id("f"), Ok(()));
assert_eq!(validate_flow_id(&"f".repeat(256)), Ok(()));
assert_eq!(
validate_flow_id(&"f".repeat(257)),
Err(OpaqueFieldError::TooLong { field, length: 257 })
);
assert_eq!(validate_flow_id("!~Flow:7/step"), Ok(()));
for bad in ["f é", "f\t", "fé", "f\u{7f}"] {
let error = validate_flow_id(bad).unwrap_err();
assert_eq!(
error,
OpaqueFieldError::InvalidCharacter { field, index: 1 }
);
assert_eq!(error.field(), "flow_id");
}
}
#[test]
fn flow_only_attributes_round_trip_and_absence_keeps_the_bytes() {
let attributes = ScopeAttributes::default();
assert!(attributes.is_empty());
assert_eq!(serde_json::to_string(&attributes).unwrap(), "{}");
assert_eq!(
serde_json::from_str::<ScopeAttributes>("{}").unwrap(),
attributes
);
let attributes = ScopeAttributes {
flow_id: Some("flow:7".to_string()),
..ScopeAttributes::default()
};
assert!(!attributes.is_empty());
let encoded = serde_json::to_string(&attributes).unwrap();
assert_eq!(encoded, r#"{"flow_id":"flow:7"}"#);
assert_eq!(
serde_json::from_str::<ScopeAttributes>(&encoded).unwrap(),
attributes
);
assert!(
serde_json::from_str::<ScopeAttributes>(r#"{"flow_id":"flow:7","unknown":true}"#)
.is_err()
);
}
}