1use super::{
2 error::IdentityError,
3 tool_alias,
4 types::{DialectId, IdNamespace, IdentityRole, KnownIdPrefix, SourceIdentity},
5};
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeSet, HashMap, HashSet};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum IdSyntax {
13 AnyString,
14 AsciiIdentifier,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct TargetIdPolicy {
21 pub dialect: DialectId,
22 pub max_len: Option<usize>,
23 pub preserve_source_ids: bool,
24 pub syntax: IdSyntax,
25 pub required_prefix: Option<KnownIdPrefix>,
26 pub allowed_generated_prefixes: Option<Vec<KnownIdPrefix>>,
27 pub generated_prefix_overrides: Vec<(IdentityRole, KnownIdPrefix)>,
28 #[serde(default)]
34 pub reversible_tool_calls: bool,
35}
36
37impl TargetIdPolicy {
38 pub fn new(dialect: DialectId) -> Self {
39 Self {
40 dialect,
41 max_len: None,
42 preserve_source_ids: true,
43 syntax: IdSyntax::AnyString,
44 required_prefix: None,
45 allowed_generated_prefixes: None,
46 generated_prefix_overrides: Vec::new(),
47 reversible_tool_calls: false,
48 }
49 }
50
51 pub fn with_reversible_tool_calls(mut self) -> Self {
52 self.reversible_tool_calls = true;
53 self
54 }
55
56 pub fn with_generated_prefixes(
57 mut self,
58 prefixes: impl IntoIterator<Item = KnownIdPrefix>,
59 ) -> Self {
60 self.allowed_generated_prefixes = Some(prefixes.into_iter().collect());
61 self
62 }
63
64 pub fn with_generated_prefix(mut self, role: IdentityRole, prefix: KnownIdPrefix) -> Self {
65 self.generated_prefix_overrides.push((role, prefix));
66 self
67 }
68
69 pub fn with_max_len(mut self, max_len: usize) -> Self {
70 self.max_len = Some(max_len);
71 self
72 }
73
74 pub fn with_syntax(mut self, syntax: IdSyntax) -> Self {
75 self.syntax = syntax;
76 self
77 }
78
79 pub fn with_required_prefix(mut self, prefix: KnownIdPrefix) -> Self {
82 self.required_prefix = Some(prefix);
83 self
84 }
85
86 fn accepts_value(&self, value: &str) -> bool {
87 valid_id(value, self.max_len)
88 && (self.syntax == IdSyntax::AnyString
89 || value
90 .bytes()
91 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'))
92 && self.required_prefix.is_none_or(|p| {
93 value
94 .strip_prefix(p.as_str())
95 .is_some_and(|suffix| !suffix.is_empty())
96 })
97 }
98
99 pub fn accepts_source(&self, value: &str) -> bool {
100 self.preserve_source_ids && self.accepts_value(value)
101 }
102
103 pub fn accepts_generated(&self, prefix: KnownIdPrefix, value: &str) -> bool {
104 self.allowed_generated_prefixes
105 .as_ref()
106 .is_none_or(|prefixes| prefixes.contains(&prefix))
107 && self.accepts_value(value)
108 }
109}
110
111fn valid_id(value: &str, max_len: Option<usize>) -> bool {
112 !value.is_empty() && max_len.is_none_or(|max| value.len() <= max)
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116struct Locator {
117 role: IdentityRole,
118 dialect: DialectId,
119 logical_index: u64,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Hash)]
123struct EmittedLookup {
124 role: IdentityRole,
125 id: String,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Hash)]
129struct SourceLookup {
130 role: IdentityRole,
131 dialect: DialectId,
132 source_id: String,
133}
134
135#[derive(Debug, Clone)]
136struct Entry {
137 source: SourceIdentity,
138 source_role: IdentityRole,
139 emitted_id: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct IdentityHandle {
145 pub role: IdentityRole,
146 pub source_role: IdentityRole,
147 namespace: IdNamespace,
148 pub source: SourceIdentity,
149 pub emitted_id: String,
150}
151
152impl IdentityHandle {
153 pub fn client_id(&self) -> &str {
155 &self.emitted_id
156 }
157
158 pub fn source_id(&self) -> Option<&str> {
161 self.source.source_id.as_deref()
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct CallResultLink {
167 pub call_id: String,
168 pub output_item_id: Option<String>,
169}
170
171#[derive(Debug, Clone)]
174pub struct IdentityFlow {
175 namespace: IdNamespace,
176 used_ids: HashSet<EmittedLookup>,
177 entries: HashMap<Locator, Entry>,
178 by_source: HashMap<SourceLookup, Locator>,
179 by_emitted: HashMap<EmittedLookup, Locator>,
180 bound_target: Option<TargetIdPolicy>,
181}
182
183impl IdentityFlow {
184 pub fn new(namespace: IdNamespace) -> Self {
185 Self {
186 namespace,
187 used_ids: HashSet::new(),
188 entries: HashMap::new(),
189 by_source: HashMap::new(),
190 by_emitted: HashMap::new(),
191 bound_target: None,
192 }
193 }
194
195 pub(crate) fn reserve_external_ids(
197 &mut self,
198 role: IdentityRole,
199 ids: &BTreeSet<String>,
200 ) -> Result<(), IdentityError> {
201 if ids.iter().any(|id| {
202 self.by_emitted.contains_key(&EmittedLookup {
203 role,
204 id: id.clone(),
205 })
206 }) {
207 return Err(IdentityError::InvalidIdentity(
208 "external identity already emitted by this call".into(),
209 ));
210 }
211 for id in ids {
212 self.used_ids.insert(EmittedLookup {
213 role,
214 id: id.clone(),
215 });
216 }
217 Ok(())
218 }
219
220 pub fn handles(&self) -> impl Iterator<Item = IdentityHandle> + '_ {
223 self.entries
224 .iter()
225 .map(|(locator, entry)| handle_from_entry(self.namespace, locator.role, entry))
226 }
227
228 pub fn namespace(&self) -> IdNamespace {
229 self.namespace
230 }
231
232 pub fn resolve_or_allocate(
234 &mut self,
235 role: IdentityRole,
236 source: SourceIdentity,
237 target: &TargetIdPolicy,
238 ) -> Result<IdentityHandle, IdentityError> {
239 self.resolve_as(role, role, source, target)
240 }
241
242 pub fn resolve_as(
245 &mut self,
246 source_role: IdentityRole,
247 role: IdentityRole,
248 source: SourceIdentity,
249 target: &TargetIdPolicy,
250 ) -> Result<IdentityHandle, IdentityError> {
251 self.resolve_inner(source_role, role, source, target, &BTreeSet::new(), false)
252 }
253
254 pub fn resolve_legacy_chat_call(
258 &mut self,
259 source: SourceIdentity,
260 target: &TargetIdPolicy,
261 ) -> Result<IdentityHandle, IdentityError> {
262 let role = IdentityRole::ToolCall;
263 self.resolve_inner(role, role, source, target, &BTreeSet::new(), true)
264 }
265
266 pub fn resolve_or_allocate_avoiding(
269 &mut self,
270 role: IdentityRole,
271 source: SourceIdentity,
272 target: &TargetIdPolicy,
273 avoid: &BTreeSet<String>,
274 ) -> Result<IdentityHandle, IdentityError> {
275 self.resolve_inner(role, role, source, target, avoid, false)
276 }
277
278 fn resolve_inner(
279 &mut self,
280 source_role: IdentityRole,
281 role: IdentityRole,
282 mut source: SourceIdentity,
283 target: &TargetIdPolicy,
284 avoid: &BTreeSet<String>,
285 legacy: bool,
286 ) -> Result<IdentityHandle, IdentityError> {
287 source.source_id = source.source_id.filter(|id| !id.is_empty());
288 if let Some(bound) = &self.bound_target
289 && bound != target
290 {
291 return Err(IdentityError::InvalidIdentity(
292 "identity flow target policy changed after allocation".into(),
293 ));
294 }
295 let locator = Locator {
296 role,
297 dialect: source.dialect,
298 logical_index: source.logical_index,
299 };
300 if let Some(source_id) = source.source_id.as_deref() {
301 let source_key = SourceLookup {
302 role,
303 dialect: source.dialect,
304 source_id: source_id.to_owned(),
305 };
306 if let Some(previous) = self.by_source.get(&source_key)
307 && previous.logical_index != source.logical_index
308 {
309 return Err(IdentityError::DuplicateSourceIdentity {
310 role: role.name(),
311 dialect: source.dialect.id().to_owned(),
312 source_id: source_id.to_owned(),
313 first_index: previous.logical_index,
314 duplicate_index: source.logical_index,
315 });
316 }
317 if let Some(entry) = self.entries.get_mut(&locator) {
318 if entry.source_role != source_role {
319 return Err(IdentityError::InvalidIdentity(
320 "source role changed at logical position".into(),
321 ));
322 }
323 if entry.source.source_id.as_deref() != Some(source_id) {
324 attach_source_inner(
325 &mut self.by_source,
326 &source_key,
327 &locator,
328 &mut entry.source,
329 )?;
330 }
331 if self.bound_target.is_none() {
332 self.bound_target = Some(target.clone());
333 }
334 return Ok(handle_from_entry(self.namespace, role, entry));
335 }
336 } else if role == IdentityRole::Resource {
337 return Err(IdentityError::ResourceIdRequired);
338 }
339
340 if let Some(entry) = self.entries.get(&locator) {
341 if entry.source_role != source_role {
342 return Err(IdentityError::InvalidIdentity(
343 "source role changed at logical position".into(),
344 ));
345 }
346 if self.bound_target.is_none() {
347 self.bound_target = Some(target.clone());
348 }
349 return Ok(handle_from_entry(self.namespace, role, entry));
350 }
351
352 let emitted_id = match (source.source_id.as_deref(), role) {
353 (_, IdentityRole::ToolCall)
354 if source_role == role
355 && target.reversible_tool_calls
356 && source.dialect != target.dialect =>
357 {
358 self.tool_alias(&source, target, avoid, legacy)?
359 }
360 (Some(source_id), _)
361 if source_role == role
362 && target.accepts_source(source_id)
363 && !avoid.contains(source_id)
364 && !self.used_ids.contains(&EmittedLookup {
365 role,
366 id: source_id.to_owned(),
367 }) =>
368 {
369 source_id.to_owned()
370 }
371 (Some(_), IdentityRole::Resource) => return Err(IdentityError::ResourceIdRequired),
372 (_, IdentityRole::Resource) => return Err(IdentityError::ResourceIdRequired),
373 _ => self.allocate_generated(role, source.dialect, source.logical_index, target)?,
374 };
375 let entry = Entry {
376 source: source.clone(),
377 source_role,
378 emitted_id: emitted_id.clone(),
379 };
380 self.used_ids.insert(EmittedLookup {
381 role,
382 id: emitted_id.clone(),
383 });
384 self.by_emitted.insert(
385 EmittedLookup {
386 role,
387 id: emitted_id.clone(),
388 },
389 locator.clone(),
390 );
391 if let Some(source_id) = source.source_id.as_deref() {
392 self.by_source.insert(
393 SourceLookup {
394 role,
395 dialect: source.dialect,
396 source_id: source_id.to_owned(),
397 },
398 locator.clone(),
399 );
400 }
401 self.entries.insert(locator, entry.clone());
402 self.bound_target = Some(target.clone());
403 Ok(handle_from_entry(self.namespace, role, &entry))
404 }
405
406 fn tool_alias(
412 &self,
413 source: &SourceIdentity,
414 target: &TargetIdPolicy,
415 avoid: &BTreeSet<String>,
416 legacy: bool,
417 ) -> Result<String, IdentityError> {
418 let prefix = generated_prefix(IdentityRole::ToolCall, target)?;
419 let free = |id: &str| {
420 !avoid.contains(id)
421 && !self.used_ids.contains(&EmittedLookup {
422 role: IdentityRole::ToolCall,
423 id: id.to_owned(),
424 })
425 };
426 let Some(id) = source.source_id.as_deref() else {
427 let namespace = self.namespace.hex();
428 let alias = tool_alias::missing(
429 prefix.as_str(),
430 &namespace[..16],
431 source.logical_index,
432 legacy,
433 );
434 if !target.accepts_generated(prefix, &alias) {
435 return Err(IdentityError::InvalidIdentity(
436 "target policy cannot accept generated identifier".into(),
437 ));
438 }
439 if !free(&alias) {
440 return Err(IdentityError::Collision(alias));
441 }
442 return Ok(alias);
443 };
444 if target.accepts_source(id)
445 && tool_alias::accepts(target.dialect, id)
446 && tool_alias::decode(id).is_none()
447 && free(id)
448 {
449 return Ok(id.to_owned());
450 }
451 let tries = self.used_ids.len() as u64 + avoid.len() as u64;
453 for repeat in 0..=tries {
454 let alias = tool_alias::upstream(prefix.as_str(), id, repeat);
455 if !target.accepts_generated(prefix, &alias) {
456 return Err(IdentityError::InvalidIdentity(
457 "target policy cannot accept a tool call alias".into(),
458 ));
459 }
460 if free(&alias) {
461 return Ok(alias);
462 }
463 }
464 Err(IdentityError::Collision(id.to_owned()))
465 }
466
467 fn allocate_generated(
468 &self,
469 role: IdentityRole,
470 source_dialect: DialectId,
471 logical_index: u64,
472 target: &TargetIdPolicy,
473 ) -> Result<String, IdentityError> {
474 let prefix = generated_prefix(role, target)?;
475 let candidate = format!(
476 "{}{}_{}_{}_{:x}",
477 prefix.as_str(),
478 self.namespace.hex(),
479 dialect_key(source_dialect),
480 role.allocation_key(),
481 logical_index
482 );
483 if !target.accepts_generated(prefix, &candidate) {
484 return Err(IdentityError::InvalidIdentity(
485 "target policy cannot accept generated identifier".into(),
486 ));
487 }
488 if self.used_ids.contains(&EmittedLookup {
489 role,
490 id: candidate.clone(),
491 }) {
492 return Err(IdentityError::Collision(candidate));
494 }
495 Ok(candidate)
496 }
497
498 pub fn attach_source(
501 &mut self,
502 handle: &IdentityHandle,
503 source_id: impl Into<String>,
504 ) -> Result<IdentityHandle, IdentityError> {
505 let source_id = source_id.into();
506 if source_id.is_empty() {
507 return Err(IdentityError::InvalidIdentity(
508 "late source id is invalid".into(),
509 ));
510 }
511 let locator = self
512 .by_emitted
513 .get(&EmittedLookup {
514 role: handle.role,
515 id: handle.emitted_id.clone(),
516 })
517 .cloned()
518 .ok_or_else(|| IdentityError::AmbiguousSourceIdentity("unknown emitted id".into()))?;
519 let entry = self
520 .entries
521 .get_mut(&locator)
522 .ok_or_else(|| IdentityError::AmbiguousSourceIdentity("missing flow entry".into()))?;
523 let source_key = SourceLookup {
524 role: locator.role,
525 dialect: locator.dialect,
526 source_id: source_id.clone(),
527 };
528 attach_source_inner(
529 &mut self.by_source,
530 &source_key,
531 &locator,
532 &mut entry.source,
533 )?;
534 Ok(handle_from_entry(self.namespace, locator.role, entry))
535 }
536
537 pub fn lookup_source(
538 &self,
539 source: &SourceIdentity,
540 role: IdentityRole,
541 ) -> Option<IdentityHandle> {
542 let source_id = source.source_id.as_deref()?;
543 let key = SourceLookup {
544 role,
545 dialect: source.dialect,
546 source_id: source_id.to_owned(),
547 };
548 let locator = self.by_source.get(&key)?;
549 self.entries
550 .get(locator)
551 .map(|entry| handle_from_entry(self.namespace, role, entry))
552 }
553
554 pub fn lookup_logical(
556 &self,
557 role: IdentityRole,
558 dialect: &DialectId,
559 logical_index: u64,
560 ) -> Option<IdentityHandle> {
561 let locator = Locator {
562 role,
563 dialect: *dialect,
564 logical_index,
565 };
566 self.entries
567 .get(&locator)
568 .map(|entry| handle_from_entry(self.namespace, role, entry))
569 }
570
571 pub fn lookup_emitted(&self, emitted_id: &str) -> Option<IdentityHandle> {
572 let mut matches = self
573 .by_emitted
574 .iter()
575 .filter(|(key, _)| key.id == emitted_id);
576 let (_, locator) = matches.next()?;
577 if matches.next().is_some() {
578 return None;
579 }
580 self.entries
581 .get(locator)
582 .map(|entry| handle_from_entry(self.namespace, locator.role, entry))
583 }
584
585 pub fn lookup_emitted_as(
586 &self,
587 role: IdentityRole,
588 emitted_id: &str,
589 ) -> Option<IdentityHandle> {
590 let locator = self.by_emitted.get(&EmittedLookup {
591 role,
592 id: emitted_id.to_owned(),
593 })?;
594 self.entries
595 .get(locator)
596 .map(|entry| handle_from_entry(self.namespace, role, entry))
597 }
598
599 pub fn lookup_client_id(&self, client_id: &str) -> Option<IdentityHandle> {
600 self.lookup_emitted(client_id)
601 }
602
603 pub fn lookup_native(
604 &self,
605 source: &SourceIdentity,
606 role: IdentityRole,
607 ) -> Option<IdentityHandle> {
608 self.lookup_source(source, role)
609 }
610
611 pub fn link_call_result(
612 &self,
613 call: &IdentityHandle,
614 result: Option<&IdentityHandle>,
615 ) -> Result<CallResultLink, IdentityError> {
616 if call.role != IdentityRole::ToolCall {
617 return Err(IdentityError::InvalidIdentity(
618 "call-result link requires a ToolCall".into(),
619 ));
620 }
621 if result.is_some_and(|result| {
622 !matches!(
623 result.role,
624 IdentityRole::OutputItem(
625 super::types::OutputItemKind::FunctionCallOutput
626 | super::types::OutputItemKind::CustomToolCallOutput
627 )
628 )
629 }) {
630 return Err(IdentityError::InvalidIdentity(
631 "call-result link requires a function/custom result item".into(),
632 ));
633 }
634 Ok(CallResultLink {
635 call_id: call.emitted_id.clone(),
636 output_item_id: result.map(|result| result.emitted_id.clone()),
637 })
638 }
639}
640
641fn attach_source_inner(
642 by_source: &mut HashMap<SourceLookup, Locator>,
643 key: &SourceLookup,
644 locator: &Locator,
645 source: &mut SourceIdentity,
646) -> Result<(), IdentityError> {
647 if let Some(previous) = by_source.get(key)
648 && previous != locator
649 {
650 return Err(IdentityError::DuplicateSourceIdentity {
651 role: locator.role.name(),
652 dialect: locator.dialect.id().to_owned(),
653 source_id: key.source_id.clone(),
654 first_index: previous.logical_index,
655 duplicate_index: locator.logical_index,
656 });
657 }
658 if let Some(existing) = source.source_id.as_deref()
659 && existing != key.source_id
660 {
661 return Err(IdentityError::AmbiguousSourceIdentity(format!(
662 "entry already has source id {existing:?}"
663 )));
664 }
665 source.source_id = Some(key.source_id.clone());
666 by_source.insert(key.clone(), locator.clone());
667 Ok(())
668}
669
670fn handle_from_entry(namespace: IdNamespace, role: IdentityRole, entry: &Entry) -> IdentityHandle {
671 IdentityHandle {
672 role,
673 source_role: entry.source_role,
674 namespace,
675 source: entry.source.clone(),
676 emitted_id: entry.emitted_id.clone(),
677 }
678}
679
680fn generated_prefix(
681 role: IdentityRole,
682 target: &TargetIdPolicy,
683) -> Result<KnownIdPrefix, IdentityError> {
684 target
685 .generated_prefix_overrides
686 .iter()
687 .rev()
688 .find_map(|(r, p)| (*r == role).then_some(*p))
689 .or_else(|| role.generated_prefix())
690 .ok_or(IdentityError::NoGeneratedPrefix)
691}
692
693fn dialect_key(dialect: DialectId) -> &'static str {
694 match dialect {
695 DialectId::OpenAi => "o",
696 DialectId::Claude => "c",
697 DialectId::Gemini => "g",
698 DialectId::OpenAiChat => "h",
699 DialectId::OpenAiResponsesWebSocket => "w",
700 }
701}