Skip to main content

a3s_code_core/
core_identity.rs

1//! Typed identity primitives shared by Code runtime projections.
2//!
3//! This module is deliberately an identity and adaptation layer, not another
4//! event store. The existing run/evaluation journal remains the append-only
5//! authority; these values let Agent, evaluation, research, and SDK adapters
6//! refer to the same operation, source, capability, and evidence identity.
7
8use crate::event_protocol::{EventEnvelopeV1, EventProtocolError};
9use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
10use std::fmt;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{SystemTime, UNIX_EPOCH};
13use thiserror::Error;
14
15pub const CORE_IDENTITY_SCHEMA_V1: &str = "a3s.code.core-identity.v1";
16pub const CORE_EVENT_IDENTITY_SCHEMA_V1: &str = "a3s.code.core-event-identity.v1";
17pub const CORE_EVENT_PAYLOAD_DIGEST_DOMAIN_V1: &str = "a3s.code.core-event.payload.v1";
18pub const CORE_EVENT_IDENTITY_DIGEST_DOMAIN_V1: &str = "a3s.code.core-event.identity.v1";
19pub const CORE_IDENTITY_MAX_ID_BYTES: usize = 256;
20pub const CORE_IDENTITY_MAX_EVENT_TYPE_BYTES: usize = 128;
21pub const CORE_IDENTITY_MAX_PAYLOAD_BYTES: usize = 4 * 1024 * 1024;
22pub const CORE_IDENTITY_MAX_MEDIA_TYPE_BYTES: usize = 256;
23pub const CORE_IDENTITY_MAX_ARTIFACT_BYTES: u64 = 4 * 1024 * 1024 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq, Error)]
26pub enum CoreIdentityError {
27    #[error("unsupported core identity schema")]
28    UnsupportedSchema,
29    #[error("core identity field `{0}` is invalid")]
30    InvalidField(&'static str),
31    #[error("core identity digest `{0}` is invalid")]
32    InvalidDigest(&'static str),
33    #[error("core identity sequence overflow")]
34    SequenceOverflow,
35    #[error("core identity serialization failed: {0}")]
36    Serialization(String),
37    #[error("agent event cannot be adapted to a core identity: {0}")]
38    EventProtocol(String),
39}
40
41/// Stable operation identity shared by all projections of one execution.
42#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
43#[serde(transparent)]
44pub struct OperationId(Box<str>);
45
46impl OperationId {
47    pub fn new(value: impl Into<String>) -> Result<Self, CoreIdentityError> {
48        let value = value.into();
49        validate_id("operation_id", &value)?;
50        Ok(Self(value.into_boxed_str()))
51    }
52
53    pub fn as_str(&self) -> &str {
54        &self.0
55    }
56}
57
58impl<'de> Deserialize<'de> for OperationId {
59    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60    where
61        D: Deserializer<'de>,
62    {
63        let value = String::deserialize(deserializer)?;
64        Self::new(value).map_err(D::Error::custom)
65    }
66}
67
68impl fmt::Display for OperationId {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        formatter.write_str(self.as_str())
71    }
72}
73
74/// Monotonic source revision used to prevent derived data crossing a source
75/// snapshot boundary. Zero means that the caller has not supplied a source
76/// revision yet; it is retained for backwards-compatible adapters.
77#[derive(
78    Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
79)]
80#[serde(transparent)]
81pub struct SourceRevision(u64);
82
83impl SourceRevision {
84    pub const fn new(value: u64) -> Self {
85        Self(value)
86    }
87
88    pub const fn unknown() -> Self {
89        Self(0)
90    }
91
92    pub const fn value(self) -> u64 {
93        self.0
94    }
95
96    pub const fn is_known(self) -> bool {
97        self.0 != 0
98    }
99
100    pub fn next(self) -> Result<Self, CoreIdentityError> {
101        self.0
102            .checked_add(1)
103            .map(Self)
104            .ok_or(CoreIdentityError::SequenceOverflow)
105    }
106}
107
108/// Exact capability publication admitted for an operation.
109#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
110#[serde(rename_all = "camelCase", deny_unknown_fields)]
111pub struct CapabilityStamp {
112    generation: u64,
113    digest: String,
114}
115
116impl CapabilityStamp {
117    pub fn new(generation: u64, digest: impl Into<String>) -> Result<Self, CoreIdentityError> {
118        if generation == 0 {
119            return Err(CoreIdentityError::InvalidField("generation"));
120        }
121        let digest = digest.into();
122        validate_digest("digest", &digest)?;
123        Ok(Self { generation, digest })
124    }
125
126    pub const fn generation(&self) -> u64 {
127        self.generation
128    }
129
130    pub fn digest(&self) -> &str {
131        &self.digest
132    }
133}
134
135impl<'de> Deserialize<'de> for CapabilityStamp {
136    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137    where
138        D: Deserializer<'de>,
139    {
140        #[derive(Deserialize)]
141        #[serde(rename_all = "camelCase", deny_unknown_fields)]
142        struct Wire {
143            generation: u64,
144            digest: String,
145        }
146
147        let wire = Wire::deserialize(deserializer)?;
148        Self::new(wire.generation, wire.digest).map_err(D::Error::custom)
149    }
150}
151
152/// Cursor into the evidence stream for one operation.
153#[derive(
154    Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
155)]
156#[serde(transparent)]
157pub struct EvidenceCursor(u64);
158
159impl EvidenceCursor {
160    pub const fn new(sequence: u64) -> Self {
161        Self(sequence)
162    }
163
164    pub const fn sequence(self) -> u64 {
165        self.0
166    }
167
168    pub fn next(self) -> Result<Self, CoreIdentityError> {
169        self.0
170            .checked_add(1)
171            .map(Self)
172            .ok_or(CoreIdentityError::SequenceOverflow)
173    }
174}
175
176/// Content-addressed artifact identity. Content remains in the authorized
177/// artifact store; this value is safe to carry through projections.
178#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
179#[serde(rename_all = "camelCase", deny_unknown_fields)]
180pub struct ArtifactRef {
181    digest: String,
182    media_type: String,
183    size_bytes: u64,
184}
185
186impl ArtifactRef {
187    pub fn new(
188        digest: impl Into<String>,
189        media_type: impl Into<String>,
190        size_bytes: u64,
191    ) -> Result<Self, CoreIdentityError> {
192        let digest = digest.into();
193        validate_digest("digest", &digest)?;
194        let media_type = media_type.into();
195        if media_type.is_empty()
196            || media_type.len() > CORE_IDENTITY_MAX_MEDIA_TYPE_BYTES
197            || media_type.contains('\0')
198            || media_type.lines().count() != 1
199        {
200            return Err(CoreIdentityError::InvalidField("media_type"));
201        }
202        if size_bytes > CORE_IDENTITY_MAX_ARTIFACT_BYTES {
203            return Err(CoreIdentityError::InvalidField("size_bytes"));
204        }
205        Ok(Self {
206            digest,
207            media_type,
208            size_bytes,
209        })
210    }
211
212    pub fn digest(&self) -> &str {
213        &self.digest
214    }
215
216    pub fn media_type(&self) -> &str {
217        &self.media_type
218    }
219
220    pub const fn size_bytes(&self) -> u64 {
221        self.size_bytes
222    }
223
224    pub fn validate(&self) -> Result<(), CoreIdentityError> {
225        Self::new(
226            self.digest.clone(),
227            self.media_type.clone(),
228            self.size_bytes,
229        )
230        .map(|_| ())
231    }
232}
233
234impl<'de> Deserialize<'de> for ArtifactRef {
235    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236    where
237        D: Deserializer<'de>,
238    {
239        #[derive(Deserialize)]
240        #[serde(rename_all = "camelCase", deny_unknown_fields)]
241        struct Wire {
242            digest: String,
243            media_type: String,
244            size_bytes: u64,
245        }
246
247        let wire = Wire::deserialize(deserializer)?;
248        Self::new(wire.digest, wire.media_type, wire.size_bytes).map_err(D::Error::custom)
249    }
250}
251
252/// Identity shared by one operation's Agent/evaluation/research projections.
253#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
254#[serde(rename_all = "camelCase", deny_unknown_fields)]
255pub struct CoreIdentity {
256    pub schema: String,
257    pub operation_id: OperationId,
258    pub source_revision: SourceRevision,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub capability_stamp: Option<CapabilityStamp>,
261    pub evidence_cursor: EvidenceCursor,
262}
263
264impl CoreIdentity {
265    pub fn new(
266        operation_id: OperationId,
267        source_revision: SourceRevision,
268        capability_stamp: Option<CapabilityStamp>,
269        evidence_cursor: EvidenceCursor,
270    ) -> Self {
271        Self {
272            schema: CORE_IDENTITY_SCHEMA_V1.to_owned(),
273            operation_id,
274            source_revision,
275            capability_stamp,
276            evidence_cursor,
277        }
278    }
279
280    pub fn validate(&self) -> Result<(), CoreIdentityError> {
281        if self.schema != CORE_IDENTITY_SCHEMA_V1 {
282            return Err(CoreIdentityError::UnsupportedSchema);
283        }
284        Ok(())
285    }
286}
287
288impl<'de> Deserialize<'de> for CoreIdentity {
289    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290    where
291        D: Deserializer<'de>,
292    {
293        #[derive(Deserialize)]
294        #[serde(rename_all = "camelCase", deny_unknown_fields)]
295        struct Wire {
296            schema: String,
297            operation_id: OperationId,
298            source_revision: SourceRevision,
299            #[serde(default)]
300            capability_stamp: Option<CapabilityStamp>,
301            evidence_cursor: EvidenceCursor,
302        }
303
304        let wire = Wire::deserialize(deserializer)?;
305        let value = Self {
306            schema: wire.schema,
307            operation_id: wire.operation_id,
308            source_revision: wire.source_revision,
309            capability_stamp: wire.capability_stamp,
310            evidence_cursor: wire.evidence_cursor,
311        };
312        value.validate().map_err(D::Error::custom)?;
313        Ok(value)
314    }
315}
316
317/// Canonical digest-only identity for one runtime event.
318#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
319#[serde(rename_all = "camelCase", deny_unknown_fields)]
320pub struct CoreEventIdentity {
321    pub schema: String,
322    pub identity: CoreIdentity,
323    pub event_type: String,
324    pub payload_digest: String,
325    pub payload_bytes: u64,
326    pub observed_at_ms: u64,
327    pub event_digest: String,
328}
329
330/// Canonical event encoding shared by the Core identity and evaluation
331/// adapters. `payload` is the type-free envelope payload; `wire` preserves the
332/// legacy runtime encoding used by existing evaluation fact digests.
333pub(crate) struct CanonicalEventPayload {
334    pub(crate) event_type: String,
335    pub(crate) payload: Vec<u8>,
336    pub(crate) wire: Vec<u8>,
337}
338
339pub(crate) fn canonical_event_payload(
340    event: &crate::AgentEvent,
341) -> Result<CanonicalEventPayload, CoreIdentityError> {
342    let envelope = EventEnvelopeV1::try_from(event).map_err(map_event_protocol_error)?;
343    let payload = serde_json::to_vec(&envelope.payload)
344        .map_err(|error| CoreIdentityError::Serialization(error.to_string()))?;
345    let wire = serde_json::to_vec(event)
346        .map_err(|error| CoreIdentityError::Serialization(error.to_string()))?;
347    Ok(CanonicalEventPayload {
348        event_type: envelope.event_type,
349        payload,
350        wire,
351    })
352}
353
354impl CoreEventIdentity {
355    pub fn from_agent_event(
356        identity: CoreIdentity,
357        observed_at_ms: u64,
358        event: &crate::AgentEvent,
359    ) -> Result<Self, CoreIdentityError> {
360        identity.validate()?;
361        let canonical = canonical_event_payload(event)?;
362        let payload = canonical.payload;
363        if payload.is_empty() || payload.len() > CORE_IDENTITY_MAX_PAYLOAD_BYTES {
364            return Err(CoreIdentityError::InvalidField("payload_bytes"));
365        }
366        let payload_bytes = u64::try_from(payload.len())
367            .map_err(|_| CoreIdentityError::InvalidField("payload_bytes"))?;
368        let mut value = Self {
369            schema: CORE_EVENT_IDENTITY_SCHEMA_V1.to_owned(),
370            identity,
371            event_type: canonical.event_type,
372            payload_digest: digest_bytes(CORE_EVENT_PAYLOAD_DIGEST_DOMAIN_V1, &payload),
373            payload_bytes,
374            observed_at_ms,
375            event_digest: String::new(),
376        };
377        value.validate_without_digest()?;
378        value.event_digest = value.expected_digest()?;
379        Ok(value)
380    }
381
382    /// Adapt an event using an injected logical clock instead of reading wall
383    /// time in the caller. This keeps deterministic replay and tests separate
384    /// from the system clock implementation.
385    pub fn from_agent_event_at(
386        identity: CoreIdentity,
387        clock: &dyn LogicalClock,
388        event: &crate::AgentEvent,
389    ) -> Result<Self, CoreIdentityError> {
390        Self::from_agent_event(identity, clock.now_ms(), event)
391    }
392
393    /// Adapt the event representation already retained by a Code run.
394    pub fn from_run_event(
395        operation_id: OperationId,
396        source_revision: SourceRevision,
397        capability_stamp: Option<CapabilityStamp>,
398        record: &crate::run::RunEventRecord,
399    ) -> Result<Self, CoreIdentityError> {
400        let sequence = u64::try_from(record.sequence)
401            .map_err(|_| CoreIdentityError::InvalidField("sequence"))?;
402        Self::from_agent_event(
403            CoreIdentity::new(
404                operation_id,
405                source_revision,
406                capability_stamp,
407                EvidenceCursor::new(sequence),
408            ),
409            record.timestamp_ms,
410            &record.event,
411        )
412    }
413
414    pub fn validate(&self) -> Result<(), CoreIdentityError> {
415        self.validate_without_digest()?;
416        validate_digest("event_digest", &self.event_digest)?;
417        if self.event_digest != self.expected_digest()? {
418            return Err(CoreIdentityError::InvalidField("event_digest"));
419        }
420        Ok(())
421    }
422
423    pub fn expected_digest(&self) -> Result<String, CoreIdentityError> {
424        #[derive(Serialize)]
425        struct Identity<'a> {
426            schema: &'a str,
427            identity: &'a CoreIdentity,
428            event_type: &'a str,
429            payload_digest: &'a str,
430            payload_bytes: u64,
431            observed_at_ms: u64,
432        }
433        digest_json(
434            CORE_EVENT_IDENTITY_DIGEST_DOMAIN_V1,
435            &Identity {
436                schema: &self.schema,
437                identity: &self.identity,
438                event_type: &self.event_type,
439                payload_digest: &self.payload_digest,
440                payload_bytes: self.payload_bytes,
441                observed_at_ms: self.observed_at_ms,
442            },
443        )
444    }
445
446    fn validate_without_digest(&self) -> Result<(), CoreIdentityError> {
447        if self.schema != CORE_EVENT_IDENTITY_SCHEMA_V1 {
448            return Err(CoreIdentityError::UnsupportedSchema);
449        }
450        self.identity.validate()?;
451        if self.event_type.is_empty()
452            || self.event_type.len() > CORE_IDENTITY_MAX_EVENT_TYPE_BYTES
453            || self.event_type.starts_with('.')
454            || self.event_type.ends_with('.')
455            || !self.event_type.bytes().all(|byte| {
456                byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
457            })
458        {
459            return Err(CoreIdentityError::InvalidField("event_type"));
460        }
461        validate_digest("payload_digest", &self.payload_digest)?;
462        if self.payload_bytes == 0 || self.payload_bytes > CORE_IDENTITY_MAX_PAYLOAD_BYTES as u64 {
463            return Err(CoreIdentityError::InvalidField("payload_bytes"));
464        }
465        if self.observed_at_ms == 0 {
466            return Err(CoreIdentityError::InvalidField("observed_at_ms"));
467        }
468        Ok(())
469    }
470}
471
472impl<'de> Deserialize<'de> for CoreEventIdentity {
473    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
474    where
475        D: Deserializer<'de>,
476    {
477        #[derive(Deserialize)]
478        #[serde(rename_all = "camelCase", deny_unknown_fields)]
479        struct Wire {
480            schema: String,
481            identity: CoreIdentity,
482            event_type: String,
483            payload_digest: String,
484            payload_bytes: u64,
485            observed_at_ms: u64,
486            event_digest: String,
487        }
488
489        let wire = Wire::deserialize(deserializer)?;
490        let value = Self {
491            schema: wire.schema,
492            identity: wire.identity,
493            event_type: wire.event_type,
494            payload_digest: wire.payload_digest,
495            payload_bytes: wire.payload_bytes,
496            observed_at_ms: wire.observed_at_ms,
497            event_digest: wire.event_digest,
498        };
499        value.validate().map_err(D::Error::custom)?;
500        Ok(value)
501    }
502}
503
504/// Injectable logical time source for event adapters and deterministic tests.
505pub trait LogicalClock: Send + Sync {
506    fn now_ms(&self) -> u64;
507}
508
509#[derive(Clone, Copy, Debug, Default)]
510pub struct SystemLogicalClock;
511
512impl LogicalClock for SystemLogicalClock {
513    fn now_ms(&self) -> u64 {
514        SystemTime::now()
515            .duration_since(UNIX_EPOCH)
516            .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64)
517            .unwrap_or(0)
518    }
519}
520
521#[derive(Debug)]
522pub struct ManualLogicalClock {
523    now_ms: AtomicU64,
524}
525
526impl ManualLogicalClock {
527    pub const fn new(initial_ms: u64) -> Self {
528        Self {
529            now_ms: AtomicU64::new(initial_ms),
530        }
531    }
532
533    pub fn set(&self, value: u64) {
534        self.now_ms.store(value, Ordering::SeqCst);
535    }
536
537    pub fn advance(&self, delta_ms: u64) -> Result<u64, CoreIdentityError> {
538        let mut current = self.now_ms.load(Ordering::SeqCst);
539        loop {
540            let next = current
541                .checked_add(delta_ms)
542                .ok_or(CoreIdentityError::SequenceOverflow)?;
543            match self
544                .now_ms
545                .compare_exchange(current, next, Ordering::SeqCst, Ordering::SeqCst)
546            {
547                Ok(_) => return Ok(next),
548                Err(actual) => current = actual,
549            }
550        }
551    }
552}
553
554impl LogicalClock for ManualLogicalClock {
555    fn now_ms(&self) -> u64 {
556        self.now_ms.load(Ordering::SeqCst)
557    }
558}
559
560fn map_event_protocol_error(error: EventProtocolError) -> CoreIdentityError {
561    CoreIdentityError::EventProtocol(error.to_string())
562}
563
564fn validate_id(field: &'static str, value: &str) -> Result<(), CoreIdentityError> {
565    if value.is_empty()
566        || value.len() > CORE_IDENTITY_MAX_ID_BYTES
567        || value.contains('\0')
568        || value.lines().count() != 1
569    {
570        return Err(CoreIdentityError::InvalidField(field));
571    }
572    Ok(())
573}
574
575fn validate_digest(field: &'static str, value: &str) -> Result<(), CoreIdentityError> {
576    if value.len() != 71
577        || !value.starts_with("sha256:")
578        || !value[7..]
579            .bytes()
580            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
581    {
582        return Err(CoreIdentityError::InvalidDigest(field));
583    }
584    Ok(())
585}
586
587fn digest_bytes(domain: &str, bytes: &[u8]) -> String {
588    use sha2::{Digest, Sha256};
589    let mut hasher = Sha256::new();
590    hasher.update(domain.as_bytes());
591    hasher.update([0]);
592    hasher.update(bytes);
593    format!("sha256:{:x}", hasher.finalize())
594}
595
596fn digest_json<T: Serialize>(domain: &str, value: &T) -> Result<String, CoreIdentityError> {
597    let bytes = serde_json::to_vec(value)
598        .map_err(|error| CoreIdentityError::Serialization(error.to_string()))?;
599    Ok(digest_bytes(domain, &bytes))
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use crate::AgentEvent;
606    use serde_json::Value;
607
608    fn digest(ch: char) -> String {
609        format!("sha256:{}", ch.to_string().repeat(64))
610    }
611
612    fn identity(cursor: u64) -> CoreIdentity {
613        CoreIdentity::new(
614            OperationId::new("session-1/run-1").unwrap(),
615            SourceRevision::new(7),
616            Some(CapabilityStamp::new(3, digest('a')).unwrap()),
617            EvidenceCursor::new(cursor),
618        )
619    }
620
621    #[test]
622    fn event_adapter_binds_typed_identity_and_is_replay_stable() {
623        let first = CoreEventIdentity::from_agent_event(
624            identity(4),
625            42,
626            &AgentEvent::TextDelta {
627                text: "evidence".to_owned(),
628            },
629        )
630        .unwrap();
631        let second = CoreEventIdentity::from_agent_event(
632            identity(4),
633            42,
634            &AgentEvent::TextDelta {
635                text: "evidence".to_owned(),
636            },
637        )
638        .unwrap();
639
640        assert_eq!(first, second);
641        assert_eq!(first.identity.evidence_cursor.sequence(), 4);
642        assert_eq!(first.event_type, "text_delta");
643        assert!(first.validate().is_ok());
644        assert!(
645            serde_json::from_value::<CoreEventIdentity>(serde_json::to_value(&first).unwrap())
646                .is_ok()
647        );
648    }
649
650    #[test]
651    fn retained_run_event_uses_its_cursor_and_observation_time() {
652        let record = crate::run::RunEventRecord {
653            sequence: 9,
654            timestamp_ms: 77,
655            event: AgentEvent::TextDelta {
656                text: "done".to_owned(),
657            },
658        };
659        let projected = record
660            .core_identity(
661                OperationId::new("session-1/run-1").unwrap(),
662                SourceRevision::new(8),
663                None,
664            )
665            .unwrap();
666        assert_eq!(projected.identity.evidence_cursor.sequence(), 9);
667        assert_eq!(projected.observed_at_ms, 77);
668        assert_eq!(projected.event_type, "text_delta");
669    }
670
671    #[test]
672    fn deserialization_rejects_tampered_digest_and_unknown_fields() {
673        let event = CoreEventIdentity::from_agent_event(
674            identity(0),
675            42,
676            &AgentEvent::Start {
677                prompt: "run".to_owned(),
678            },
679        )
680        .unwrap();
681        let mut value = serde_json::to_value(&event).unwrap();
682        value["eventDigest"] = Value::String(digest('b'));
683        assert!(serde_json::from_value::<CoreEventIdentity>(value).is_err());
684
685        let mut value = serde_json::to_value(&event).unwrap();
686        value["unexpected"] = Value::Bool(true);
687        assert!(serde_json::from_value::<CoreEventIdentity>(value).is_err());
688
689        let artifact = ArtifactRef::new(digest('a'), "text/plain", 3).unwrap();
690        let mut value = serde_json::to_value(&artifact).unwrap();
691        value["sizeBytes"] = Value::from(CORE_IDENTITY_MAX_ARTIFACT_BYTES + 1);
692        assert!(serde_json::from_value::<ArtifactRef>(value).is_err());
693    }
694
695    #[test]
696    fn manual_clock_is_injectable_and_overflow_is_explicit() {
697        let clock = ManualLogicalClock::new(10);
698        assert_eq!(clock.now_ms(), 10);
699        assert_eq!(clock.advance(5).unwrap(), 15);
700        clock.set(u64::MAX);
701        assert_eq!(clock.advance(1), Err(CoreIdentityError::SequenceOverflow));
702    }
703
704    #[test]
705    fn event_adapter_can_use_an_injected_clock() {
706        let clock = ManualLogicalClock::new(123);
707        let event = CoreEventIdentity::from_agent_event_at(
708            identity(0),
709            &clock,
710            &AgentEvent::Start {
711                prompt: "run".to_owned(),
712            },
713        )
714        .unwrap();
715        assert_eq!(event.observed_at_ms, 123);
716        clock.advance(7).unwrap();
717        let next = CoreEventIdentity::from_agent_event_at(
718            identity(1),
719            &clock,
720            &AgentEvent::TextDelta {
721                text: "step".to_owned(),
722            },
723        )
724        .unwrap();
725        assert_eq!(next.observed_at_ms, 130);
726    }
727
728    #[test]
729    fn typed_values_reject_invalid_wire_data() {
730        assert!(OperationId::new("bad\noperation").is_err());
731        assert!(CapabilityStamp::new(0, digest('a')).is_err());
732        assert!(ArtifactRef::new(digest('a'), "", 0).is_err());
733        assert_eq!(
734            EvidenceCursor::new(u64::MAX).next(),
735            Err(CoreIdentityError::SequenceOverflow)
736        );
737    }
738}