Skip to main content

a3s_code_core/
agent_protocol.rs

1//! Versioned headless protocol owned by A3S Code.
2//!
3//! Cloud and other hosts may transport these values, but A3S Code remains the
4//! authority for Agent session/run lifecycle, event names, cancellation, and
5//! checkpoint recovery. The protocol intentionally contains no Cloud tenant,
6//! scheduler, Workload, Runtime, or provider identity.
7
8use crate::event_protocol::{run_event_envelope_v1, EventEnvelopeV1, EVENT_ENVELOPE_V1_VERSION};
9pub use crate::release::AGENT_PROTOCOL_V1;
10use crate::run::{RunEventPage, RunEventRecord, RunStatus};
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use thiserror::Error;
14
15pub const AGENT_PROTOCOL_MAX_ID_BYTES: usize = 256;
16pub const AGENT_PROTOCOL_MAX_REASON_BYTES: usize = 1_024;
17pub const AGENT_PROTOCOL_MAX_PROMPT_BYTES: usize = 64 * 1024;
18pub const AGENT_PROTOCOL_MAX_EVENT_TYPE_BYTES: usize = 128;
19pub const AGENT_PROTOCOL_MAX_EVENT_PAYLOAD_BYTES: usize = 64 * 1024;
20pub const AGENT_PROTOCOL_MAX_EVENT_METADATA_BYTES: usize = 16 * 1024;
21pub const AGENT_PROTOCOL_MAX_EVENT_RECORD_BYTES: usize = 64 * 1024;
22pub const AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE: usize = 64;
23pub const AGENT_PROTOCOL_MAX_EVENT_PAGE_BYTES: usize = 6 * 1024 * 1024;
24
25/// Canonical HTTP endpoint served by `a3s code harness` for v1 commands.
26pub const AGENT_PROTOCOL_COMMAND_HTTP_PATH_V1: &str = "/v1/agent/commands";
27
28/// Canonical HTTP endpoint served by `a3s code harness` for v1 event pages.
29pub const AGENT_PROTOCOL_EVENT_PAGE_HTTP_PATH_V1: &str = "/v1/agent/events:page";
30
31/// Stable validation failures for the headless Agent protocol.
32#[derive(Debug, Clone, PartialEq, Eq, Error)]
33pub enum AgentProtocolError {
34    #[error("unsupported A3S Code Agent protocol schema")]
35    UnsupportedSchema,
36    #[error("invalid A3S Code Agent protocol field: {0}")]
37    InvalidField(&'static str),
38    #[error("A3S Code Agent protocol identity or sequence does not match")]
39    IdentityMismatch,
40    #[error("A3S Code Agent protocol value exceeds its bounded encoding")]
41    Encoding,
42}
43
44impl AgentProtocolError {
45    /// Stable machine-readable error code for SDK and service boundaries.
46    pub const fn code(&self) -> &'static str {
47        match self {
48            Self::UnsupportedSchema => "a3s.code.agent_protocol.unsupported_schema",
49            Self::InvalidField(_) => "a3s.code.agent_protocol.invalid_field",
50            Self::IdentityMismatch => "a3s.code.agent_protocol.identity_mismatch",
51            Self::Encoding => "a3s.code.agent_protocol.encoding",
52        }
53    }
54}
55
56/// Exact A3S Code release, session, and run selected by a host.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct AgentProtocolRunIdentityV1 {
60    pub schema: String,
61    pub protocol: String,
62    pub agent_release_identity: String,
63    pub session_id: String,
64    pub run_id: String,
65}
66
67impl AgentProtocolRunIdentityV1 {
68    pub const SCHEMA: &'static str = "a3s.code.agent-run-identity.v1";
69
70    pub fn validate(&self) -> Result<(), AgentProtocolError> {
71        validate_schema(&self.schema, Self::SCHEMA)?;
72        if self.protocol != AGENT_PROTOCOL_V1 {
73            return Err(AgentProtocolError::InvalidField("protocol"));
74        }
75        validate_lower_sha256("agent_release_identity", &self.agent_release_identity)?;
76        validate_id("session_id", &self.session_id)?;
77        validate_id("run_id", &self.run_id)
78    }
79
80    pub fn digest(&self) -> Result<String, AgentProtocolError> {
81        digest_validated(self, || self.validate())
82    }
83}
84
85/// Start a fresh A3S Code run with an exact host-selected identity.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct AgentProtocolRunStartV1 {
89    pub schema: String,
90    pub request_id: String,
91    pub identity: AgentProtocolRunIdentityV1,
92    pub prompt: String,
93}
94
95impl AgentProtocolRunStartV1 {
96    pub const SCHEMA: &'static str = "a3s.code.agent-run-start.v1";
97
98    pub fn validate(&self) -> Result<(), AgentProtocolError> {
99        validate_schema(&self.schema, Self::SCHEMA)?;
100        validate_id("request_id", &self.request_id)?;
101        self.identity.validate()?;
102        if self.prompt.trim().is_empty()
103            || self.prompt.len() > AGENT_PROTOCOL_MAX_PROMPT_BYTES
104            || self.prompt.contains('\0')
105        {
106            return Err(AgentProtocolError::InvalidField("prompt"));
107        }
108        Ok(())
109    }
110}
111
112/// Cancel the current exact A3S Code run.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct AgentProtocolRunCancelV1 {
116    pub schema: String,
117    pub request_id: String,
118    pub identity: AgentProtocolRunIdentityV1,
119    pub reason: String,
120}
121
122impl AgentProtocolRunCancelV1 {
123    pub const SCHEMA: &'static str = "a3s.code.agent-run-cancel.v1";
124
125    pub fn validate(&self) -> Result<(), AgentProtocolError> {
126        validate_schema(&self.schema, Self::SCHEMA)?;
127        validate_id("request_id", &self.request_id)?;
128        self.identity.validate()?;
129        validate_single_line("reason", &self.reason, AGENT_PROTOCOL_MAX_REASON_BYTES)
130    }
131}
132
133/// Resume an A3S Code loop checkpoint into a fresh exact run identity.
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct AgentProtocolRunRecoverV1 {
137    pub schema: String,
138    pub request_id: String,
139    pub identity: AgentProtocolRunIdentityV1,
140    pub checkpoint_run_id: String,
141}
142
143impl AgentProtocolRunRecoverV1 {
144    pub const SCHEMA: &'static str = "a3s.code.agent-run-recover.v1";
145
146    pub fn validate(&self) -> Result<(), AgentProtocolError> {
147        validate_schema(&self.schema, Self::SCHEMA)?;
148        validate_id("request_id", &self.request_id)?;
149        self.identity.validate()?;
150        validate_id("checkpoint_run_id", &self.checkpoint_run_id)?;
151        if self.checkpoint_run_id == self.identity.run_id {
152            return Err(AgentProtocolError::InvalidField("checkpoint_run_id"));
153        }
154        Ok(())
155    }
156}
157
158/// Closed actions accepted by the version-one Code Agent protocol.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum AgentProtocolCommandActionV1 {
162    Start,
163    Cancel,
164    Recover,
165}
166
167/// One typed command for the A3S Code session/run lifecycle.
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
170pub enum AgentProtocolCommandV1 {
171    Start { request: AgentProtocolRunStartV1 },
172    Cancel { request: AgentProtocolRunCancelV1 },
173    Recover { request: AgentProtocolRunRecoverV1 },
174}
175
176impl AgentProtocolCommandV1 {
177    pub const fn action(&self) -> AgentProtocolCommandActionV1 {
178        match self {
179            Self::Start { .. } => AgentProtocolCommandActionV1::Start,
180            Self::Cancel { .. } => AgentProtocolCommandActionV1::Cancel,
181            Self::Recover { .. } => AgentProtocolCommandActionV1::Recover,
182        }
183    }
184
185    pub fn request_id(&self) -> &str {
186        match self {
187            Self::Start { request } => &request.request_id,
188            Self::Cancel { request } => &request.request_id,
189            Self::Recover { request } => &request.request_id,
190        }
191    }
192
193    pub fn identity(&self) -> &AgentProtocolRunIdentityV1 {
194        match self {
195            Self::Start { request } => &request.identity,
196            Self::Cancel { request } => &request.identity,
197            Self::Recover { request } => &request.identity,
198        }
199    }
200
201    pub fn validate(&self) -> Result<(), AgentProtocolError> {
202        match self {
203            Self::Start { request } => request.validate(),
204            Self::Cancel { request } => request.validate(),
205            Self::Recover { request } => request.validate(),
206        }
207    }
208
209    pub fn digest(&self) -> Result<String, AgentProtocolError> {
210        digest_validated(self, || self.validate())
211    }
212}
213
214/// Stable wire projection of [`RunStatus`].
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum AgentProtocolRunStateV1 {
218    Created,
219    Planning,
220    Executing,
221    Verifying,
222    Completed,
223    Failed,
224    Cancelled,
225}
226
227impl AgentProtocolRunStateV1 {
228    pub const fn as_str(self) -> &'static str {
229        match self {
230            Self::Created => "created",
231            Self::Planning => "planning",
232            Self::Executing => "executing",
233            Self::Verifying => "verifying",
234            Self::Completed => "completed",
235            Self::Failed => "failed",
236            Self::Cancelled => "cancelled",
237        }
238    }
239
240    pub const fn is_terminal(self) -> bool {
241        matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
242    }
243}
244
245impl From<RunStatus> for AgentProtocolRunStateV1 {
246    fn from(value: RunStatus) -> Self {
247        match value {
248            RunStatus::Created => Self::Created,
249            RunStatus::Planning => Self::Planning,
250            RunStatus::Executing => Self::Executing,
251            RunStatus::Verifying => Self::Verifying,
252            RunStatus::Completed => Self::Completed,
253            RunStatus::Failed => Self::Failed,
254            RunStatus::Cancelled => Self::Cancelled,
255        }
256    }
257}
258
259/// Exact observation returned after A3S Code accepts a command.
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(deny_unknown_fields)]
262pub struct AgentProtocolCommandReceiptV1 {
263    pub schema: String,
264    pub action: AgentProtocolCommandActionV1,
265    pub request_id: String,
266    pub identity: AgentProtocolRunIdentityV1,
267    pub command_digest: String,
268    pub state: AgentProtocolRunStateV1,
269    pub latest_event_sequence_exclusive: u64,
270    pub observed_at_ms: u64,
271    pub replayed: bool,
272}
273
274impl AgentProtocolCommandReceiptV1 {
275    pub const SCHEMA: &'static str = "a3s.code.agent-command-receipt.v1";
276
277    pub fn validate(&self) -> Result<(), AgentProtocolError> {
278        validate_schema(&self.schema, Self::SCHEMA)?;
279        validate_id("request_id", &self.request_id)?;
280        self.identity.validate()?;
281        validate_lower_sha256("command_digest", &self.command_digest)?;
282        if self.observed_at_ms == 0 {
283            return Err(AgentProtocolError::InvalidField("observed_at_ms"));
284        }
285        Ok(())
286    }
287
288    pub fn validate_for(&self, command: &AgentProtocolCommandV1) -> Result<(), AgentProtocolError> {
289        command.validate()?;
290        self.validate()?;
291        if self.action != command.action()
292            || self.request_id != command.request_id()
293            || self.identity != *command.identity()
294            || self.command_digest != command.digest()?
295        {
296            return Err(AgentProtocolError::IdentityMismatch);
297        }
298        if self.action == AgentProtocolCommandActionV1::Cancel && !self.state.is_terminal() {
299            return Err(AgentProtocolError::InvalidField("state"));
300        }
301        Ok(())
302    }
303}
304
305/// One authoritative A3S Code event at its run-local sequence.
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct AgentProtocolEventRecordV1 {
309    pub sequence: u64,
310    pub occurred_at_ms: u64,
311    pub event: EventEnvelopeV1,
312}
313
314impl AgentProtocolEventRecordV1 {
315    pub fn from_run_event(
316        record: &RunEventRecord,
317        identity: &AgentProtocolRunIdentityV1,
318    ) -> Result<Self, AgentProtocolError> {
319        identity.validate()?;
320        let sequence = u64::try_from(record.sequence)
321            .map_err(|_| AgentProtocolError::InvalidField("sequence"))?;
322        let event = run_event_envelope_v1(record, &identity.run_id, &identity.session_id)
323            .map_err(|_| AgentProtocolError::Encoding)?;
324        let projected = Self {
325            sequence,
326            occurred_at_ms: record.timestamp_ms,
327            event,
328        };
329        projected.validate_for(identity)?;
330        Ok(projected)
331    }
332
333    /// Validate this exact record against its Code-owned run identity.
334    ///
335    /// Hosts use this at durable ingestion boundaries instead of copying the
336    /// event metadata and sequence rules into their own protocol layer.
337    pub fn validate_for(
338        &self,
339        identity: &AgentProtocolRunIdentityV1,
340    ) -> Result<(), AgentProtocolError> {
341        if self.event.version != EVENT_ENVELOPE_V1_VERSION {
342            return Err(AgentProtocolError::InvalidField("event.version"));
343        }
344        validate_single_line(
345            "event.type",
346            &self.event.event_type,
347            AGENT_PROTOCOL_MAX_EVENT_TYPE_BYTES,
348        )?;
349        validate_json_size(
350            "event.payload",
351            &self.event.payload,
352            AGENT_PROTOCOL_MAX_EVENT_PAYLOAD_BYTES,
353        )?;
354        let metadata = self
355            .event
356            .metadata
357            .as_ref()
358            .ok_or(AgentProtocolError::InvalidField("event.metadata"))?;
359        validate_json_size(
360            "event.metadata",
361            metadata,
362            AGENT_PROTOCOL_MAX_EVENT_METADATA_BYTES,
363        )?;
364        let metadata = metadata
365            .as_object()
366            .ok_or(AgentProtocolError::InvalidField("event.metadata"))?;
367        let exact = metadata.get("session_id").and_then(|value| value.as_str())
368            == Some(identity.session_id.as_str())
369            && metadata.get("run_id").and_then(|value| value.as_str())
370                == Some(identity.run_id.as_str())
371            && metadata.get("sequence").and_then(|value| value.as_u64()) == Some(self.sequence)
372            && metadata
373                .get("timestamp_ms")
374                .and_then(|value| value.as_u64())
375                == Some(self.occurred_at_ms);
376        if !exact {
377            return Err(AgentProtocolError::IdentityMismatch);
378        }
379        let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
380        if encoded.len() > AGENT_PROTOCOL_MAX_EVENT_RECORD_BYTES {
381            return Err(AgentProtocolError::InvalidField("event"));
382        }
383        Ok(())
384    }
385}
386
387/// Bounded cursor query accepted by the A3S Code Harness event endpoint.
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389#[serde(deny_unknown_fields)]
390pub struct AgentProtocolEventPageRequestV1 {
391    pub schema: String,
392    pub identity: AgentProtocolRunIdentityV1,
393    pub after_event_sequence: Option<u64>,
394    pub limit: u16,
395}
396
397impl AgentProtocolEventPageRequestV1 {
398    pub const SCHEMA: &'static str = "a3s.code.agent-event-page-request.v1";
399
400    pub fn validate(&self) -> Result<(), AgentProtocolError> {
401        validate_schema(&self.schema, Self::SCHEMA)?;
402        self.identity.validate()?;
403        if self.limit == 0 || usize::from(self.limit) > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
404            return Err(AgentProtocolError::InvalidField("limit"));
405        }
406        Ok(())
407    }
408
409    pub fn digest(&self) -> Result<String, AgentProtocolError> {
410        digest_validated(self, || self.validate())
411    }
412}
413
414/// Cursor page projected directly from A3S Code's authoritative run store.
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416#[serde(deny_unknown_fields)]
417pub struct AgentProtocolEventPageV1 {
418    pub schema: String,
419    pub identity: AgentProtocolRunIdentityV1,
420    pub after_event_sequence: Option<u64>,
421    pub first_available_sequence: Option<u64>,
422    pub latest_sequence_exclusive: u64,
423    pub next_after_event_sequence: Option<u64>,
424    pub state: AgentProtocolRunStateV1,
425    pub observed_at_ms: u64,
426    pub retention_gap: bool,
427    pub has_more: bool,
428    pub events: Vec<AgentProtocolEventRecordV1>,
429}
430
431impl AgentProtocolEventPageV1 {
432    pub const SCHEMA: &'static str = "a3s.code.agent-event-page.v1";
433
434    pub fn from_run_page(
435        identity: AgentProtocolRunIdentityV1,
436        state: RunStatus,
437        observed_at_ms: u64,
438        after_event_sequence: Option<usize>,
439        page: &RunEventPage,
440    ) -> Result<Self, AgentProtocolError> {
441        identity.validate()?;
442        let convert = |value: usize| {
443            u64::try_from(value).map_err(|_| AgentProtocolError::InvalidField("sequence"))
444        };
445        let events = page
446            .events
447            .iter()
448            .map(|record| AgentProtocolEventRecordV1::from_run_event(record, &identity))
449            .collect::<Result<Vec<_>, _>>()?;
450        let projected = Self {
451            schema: Self::SCHEMA.into(),
452            identity,
453            after_event_sequence: after_event_sequence.map(convert).transpose()?,
454            first_available_sequence: page.first_available_sequence.map(convert).transpose()?,
455            latest_sequence_exclusive: convert(page.latest_sequence_exclusive)?,
456            next_after_event_sequence: page.next_after_sequence.map(convert).transpose()?,
457            state: state.into(),
458            observed_at_ms,
459            retention_gap: page.retention_gap,
460            has_more: page.has_more,
461            events,
462        };
463        projected.validate()?;
464        Ok(projected)
465    }
466
467    pub fn validate(&self) -> Result<(), AgentProtocolError> {
468        validate_schema(&self.schema, Self::SCHEMA)?;
469        self.identity.validate()?;
470        if self.events.len() > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
471            return Err(AgentProtocolError::InvalidField("events"));
472        }
473        if self
474            .first_available_sequence
475            .is_some_and(|sequence| sequence >= self.latest_sequence_exclusive)
476        {
477            return Err(AgentProtocolError::InvalidField("first_available_sequence"));
478        }
479
480        let requested_start = self
481            .after_event_sequence
482            .map(|sequence| sequence.saturating_add(1))
483            .unwrap_or(0);
484        let expected_gap = requested_start < self.latest_sequence_exclusive
485            && self
486                .first_available_sequence
487                .is_none_or(|first| requested_start < first);
488        if self.retention_gap != expected_gap {
489            return Err(AgentProtocolError::InvalidField("retention_gap"));
490        }
491
492        let mut previous: Option<(u64, u64)> = None;
493        for event in &self.events {
494            event.validate_for(&self.identity)?;
495            if event.occurred_at_ms > self.observed_at_ms
496                || previous.is_some_and(|(sequence, timestamp)| {
497                    event.sequence != sequence.saturating_add(1) || event.occurred_at_ms < timestamp
498                })
499                || event.sequence >= self.latest_sequence_exclusive
500            {
501                return Err(AgentProtocolError::InvalidField("events"));
502            }
503            previous = Some((event.sequence, event.occurred_at_ms));
504        }
505
506        if let Some(first) = self.events.first() {
507            if (!self.retention_gap && first.sequence != requested_start)
508                || (self.retention_gap && self.first_available_sequence != Some(first.sequence))
509                || self
510                    .first_available_sequence
511                    .is_some_and(|available| first.sequence < available)
512            {
513                return Err(AgentProtocolError::InvalidField("events"));
514            }
515        } else if self.retention_gap && self.first_available_sequence.is_some() {
516            return Err(AgentProtocolError::InvalidField("events"));
517        }
518        let expected_next = self
519            .events
520            .last()
521            .map(|event| event.sequence)
522            .or(self.after_event_sequence);
523        if self.next_after_event_sequence != expected_next {
524            return Err(AgentProtocolError::InvalidField(
525                "next_after_event_sequence",
526            ));
527        }
528        if self.has_more {
529            if self.events.last().is_none_or(|event| {
530                event.sequence.saturating_add(1) >= self.latest_sequence_exclusive
531            }) {
532                return Err(AgentProtocolError::InvalidField("has_more"));
533            }
534        } else if let Some(last) = self.events.last() {
535            if last.sequence.saturating_add(1) != self.latest_sequence_exclusive {
536                return Err(AgentProtocolError::InvalidField("has_more"));
537            }
538        }
539        let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
540        if encoded.len() > AGENT_PROTOCOL_MAX_EVENT_PAGE_BYTES {
541            return Err(AgentProtocolError::InvalidField("events"));
542        }
543        Ok(())
544    }
545
546    pub fn first_sequence(&self) -> Option<u64> {
547        self.events.first().map(|event| event.sequence)
548    }
549
550    pub fn last_sequence(&self) -> Option<u64> {
551        self.events.last().map(|event| event.sequence)
552    }
553
554    pub fn digest(&self) -> Result<String, AgentProtocolError> {
555        digest_validated(self, || self.validate())
556    }
557}
558
559fn validate_schema(value: &str, expected: &str) -> Result<(), AgentProtocolError> {
560    if value == expected {
561        Ok(())
562    } else {
563        Err(AgentProtocolError::UnsupportedSchema)
564    }
565}
566
567fn validate_id(field: &'static str, value: &str) -> Result<(), AgentProtocolError> {
568    validate_single_line(field, value, AGENT_PROTOCOL_MAX_ID_BYTES)
569}
570
571fn validate_single_line(
572    field: &'static str,
573    value: &str,
574    max: usize,
575) -> Result<(), AgentProtocolError> {
576    if value.trim().is_empty()
577        || value.len() > max
578        || value.contains('\0')
579        || value.contains(['\r', '\n'])
580    {
581        Err(AgentProtocolError::InvalidField(field))
582    } else {
583        Ok(())
584    }
585}
586
587pub(crate) fn validate_lower_sha256(
588    field: &'static str,
589    value: &str,
590) -> Result<(), AgentProtocolError> {
591    let valid = value.strip_prefix("sha256:").is_some_and(|hex| {
592        hex.len() == 64
593            && hex
594                .bytes()
595                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
596    });
597    if valid {
598        Ok(())
599    } else {
600        Err(AgentProtocolError::InvalidField(field))
601    }
602}
603
604fn validate_json_size(
605    field: &'static str,
606    value: &serde_json::Value,
607    max: usize,
608) -> Result<(), AgentProtocolError> {
609    let encoded = serde_json::to_vec(value).map_err(|_| AgentProtocolError::Encoding)?;
610    if encoded.len() > max {
611        Err(AgentProtocolError::InvalidField(field))
612    } else {
613        Ok(())
614    }
615}
616
617fn digest_validated<T: Serialize>(
618    value: &T,
619    validate: impl FnOnce() -> Result<(), AgentProtocolError>,
620) -> Result<String, AgentProtocolError> {
621    validate()?;
622    let encoded = serde_json::to_vec(value).map_err(|_| AgentProtocolError::Encoding)?;
623    Ok(format!("sha256:{:x}", Sha256::digest(encoded)))
624}