1use 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 base64::Engine as _;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14use thiserror::Error;
15
16pub const AGENT_PROTOCOL_MAX_ID_BYTES: usize = 256;
17pub const AGENT_PROTOCOL_MAX_REASON_BYTES: usize = 1_024;
18pub const AGENT_PROTOCOL_MAX_PROMPT_BYTES: usize = 64 * 1024;
19pub const AGENT_PROTOCOL_MAX_EVENT_TYPE_BYTES: usize = 128;
20pub const AGENT_PROTOCOL_MAX_EVENT_PAYLOAD_BYTES: usize = 64 * 1024;
21pub const AGENT_PROTOCOL_MAX_EVENT_METADATA_BYTES: usize = 16 * 1024;
22pub const AGENT_PROTOCOL_MAX_EVENT_RECORD_BYTES: usize = 64 * 1024;
23pub const AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE: usize = 64;
24pub const AGENT_PROTOCOL_MAX_EVENT_PAGE_BYTES: usize = 6 * 1024 * 1024;
25pub const AGENT_PROTOCOL_MAX_CHANGE_SET_BYTES: usize = 4 * 1024 * 1024;
26pub const AGENT_PROTOCOL_MAX_CHANGE_SET_RESPONSE_BYTES: usize =
27 (AGENT_PROTOCOL_MAX_CHANGE_SET_BYTES * 4 / 3) + 128 * 1024;
28pub const AGENT_PROTOCOL_CHANGE_SET_FORMAT_V1: &str = "git_unified_diff_v1";
29pub const AGENT_PROTOCOL_CHANGE_SET_ENCODING_V1: &str = "base64";
30
31pub const AGENT_PROTOCOL_COMMAND_HTTP_PATH_V1: &str = "/v1/agent/commands";
33
34pub const AGENT_PROTOCOL_EVENT_PAGE_HTTP_PATH_V1: &str = "/v1/agent/events:page";
36
37pub const AGENT_PROTOCOL_CHANGE_SET_HTTP_PATH_V1: &str = "/v1/agent/changes";
39
40#[derive(Debug, Clone, PartialEq, Eq, Error)]
42pub enum AgentProtocolError {
43 #[error("unsupported A3S Code Agent protocol schema")]
44 UnsupportedSchema,
45 #[error("invalid A3S Code Agent protocol field: {0}")]
46 InvalidField(&'static str),
47 #[error("A3S Code Agent protocol identity or sequence does not match")]
48 IdentityMismatch,
49 #[error("A3S Code Agent protocol value exceeds its bounded encoding")]
50 Encoding,
51}
52
53impl AgentProtocolError {
54 pub const fn code(&self) -> &'static str {
56 match self {
57 Self::UnsupportedSchema => "a3s.code.agent_protocol.unsupported_schema",
58 Self::InvalidField(_) => "a3s.code.agent_protocol.invalid_field",
59 Self::IdentityMismatch => "a3s.code.agent_protocol.identity_mismatch",
60 Self::Encoding => "a3s.code.agent_protocol.encoding",
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct AgentProtocolRunIdentityV1 {
69 pub schema: String,
70 pub protocol: String,
71 pub agent_release_identity: String,
72 pub session_id: String,
73 pub run_id: String,
74}
75
76impl AgentProtocolRunIdentityV1 {
77 pub const SCHEMA: &'static str = "a3s.code.agent-run-identity.v1";
78
79 pub fn validate(&self) -> Result<(), AgentProtocolError> {
80 validate_schema(&self.schema, Self::SCHEMA)?;
81 if self.protocol != AGENT_PROTOCOL_V1 {
82 return Err(AgentProtocolError::InvalidField("protocol"));
83 }
84 validate_lower_sha256("agent_release_identity", &self.agent_release_identity)?;
85 validate_id("session_id", &self.session_id)?;
86 validate_id("run_id", &self.run_id)
87 }
88
89 pub fn digest(&self) -> Result<String, AgentProtocolError> {
90 digest_validated(self, || self.validate())
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct AgentProtocolRunStartV1 {
98 pub schema: String,
99 pub request_id: String,
100 pub identity: AgentProtocolRunIdentityV1,
101 pub prompt: String,
102}
103
104impl AgentProtocolRunStartV1 {
105 pub const SCHEMA: &'static str = "a3s.code.agent-run-start.v1";
106
107 pub fn validate(&self) -> Result<(), AgentProtocolError> {
108 validate_schema(&self.schema, Self::SCHEMA)?;
109 validate_id("request_id", &self.request_id)?;
110 self.identity.validate()?;
111 if self.prompt.trim().is_empty()
112 || self.prompt.len() > AGENT_PROTOCOL_MAX_PROMPT_BYTES
113 || self.prompt.contains('\0')
114 {
115 return Err(AgentProtocolError::InvalidField("prompt"));
116 }
117 Ok(())
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct AgentProtocolRunCancelV1 {
125 pub schema: String,
126 pub request_id: String,
127 pub identity: AgentProtocolRunIdentityV1,
128 pub reason: String,
129}
130
131impl AgentProtocolRunCancelV1 {
132 pub const SCHEMA: &'static str = "a3s.code.agent-run-cancel.v1";
133
134 pub fn validate(&self) -> Result<(), AgentProtocolError> {
135 validate_schema(&self.schema, Self::SCHEMA)?;
136 validate_id("request_id", &self.request_id)?;
137 self.identity.validate()?;
138 validate_single_line("reason", &self.reason, AGENT_PROTOCOL_MAX_REASON_BYTES)
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct AgentProtocolRunRecoverV1 {
146 pub schema: String,
147 pub request_id: String,
148 pub identity: AgentProtocolRunIdentityV1,
149 pub checkpoint_run_id: String,
150}
151
152impl AgentProtocolRunRecoverV1 {
153 pub const SCHEMA: &'static str = "a3s.code.agent-run-recover.v1";
154
155 pub fn validate(&self) -> Result<(), AgentProtocolError> {
156 validate_schema(&self.schema, Self::SCHEMA)?;
157 validate_id("request_id", &self.request_id)?;
158 self.identity.validate()?;
159 validate_id("checkpoint_run_id", &self.checkpoint_run_id)?;
160 if self.checkpoint_run_id == self.identity.run_id {
161 return Err(AgentProtocolError::InvalidField("checkpoint_run_id"));
162 }
163 Ok(())
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum AgentProtocolCommandActionV1 {
171 Start,
172 Cancel,
173 Recover,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
179pub enum AgentProtocolCommandV1 {
180 Start { request: AgentProtocolRunStartV1 },
181 Cancel { request: AgentProtocolRunCancelV1 },
182 Recover { request: AgentProtocolRunRecoverV1 },
183}
184
185impl AgentProtocolCommandV1 {
186 pub const fn action(&self) -> AgentProtocolCommandActionV1 {
187 match self {
188 Self::Start { .. } => AgentProtocolCommandActionV1::Start,
189 Self::Cancel { .. } => AgentProtocolCommandActionV1::Cancel,
190 Self::Recover { .. } => AgentProtocolCommandActionV1::Recover,
191 }
192 }
193
194 pub fn request_id(&self) -> &str {
195 match self {
196 Self::Start { request } => &request.request_id,
197 Self::Cancel { request } => &request.request_id,
198 Self::Recover { request } => &request.request_id,
199 }
200 }
201
202 pub fn identity(&self) -> &AgentProtocolRunIdentityV1 {
203 match self {
204 Self::Start { request } => &request.identity,
205 Self::Cancel { request } => &request.identity,
206 Self::Recover { request } => &request.identity,
207 }
208 }
209
210 pub fn validate(&self) -> Result<(), AgentProtocolError> {
211 match self {
212 Self::Start { request } => request.validate(),
213 Self::Cancel { request } => request.validate(),
214 Self::Recover { request } => request.validate(),
215 }
216 }
217
218 pub fn digest(&self) -> Result<String, AgentProtocolError> {
219 digest_validated(self, || self.validate())
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub enum AgentProtocolRunStateV1 {
227 Created,
228 Planning,
229 Executing,
230 Verifying,
231 Completed,
232 Failed,
233 Cancelled,
234}
235
236impl AgentProtocolRunStateV1 {
237 pub const fn as_str(self) -> &'static str {
238 match self {
239 Self::Created => "created",
240 Self::Planning => "planning",
241 Self::Executing => "executing",
242 Self::Verifying => "verifying",
243 Self::Completed => "completed",
244 Self::Failed => "failed",
245 Self::Cancelled => "cancelled",
246 }
247 }
248
249 pub const fn is_terminal(self) -> bool {
250 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
251 }
252}
253
254impl From<RunStatus> for AgentProtocolRunStateV1 {
255 fn from(value: RunStatus) -> Self {
256 match value {
257 RunStatus::Created => Self::Created,
258 RunStatus::Planning => Self::Planning,
259 RunStatus::Executing => Self::Executing,
260 RunStatus::Verifying => Self::Verifying,
261 RunStatus::Completed => Self::Completed,
262 RunStatus::Failed => Self::Failed,
263 RunStatus::Cancelled => Self::Cancelled,
264 }
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct AgentProtocolCommandReceiptV1 {
272 pub schema: String,
273 pub action: AgentProtocolCommandActionV1,
274 pub request_id: String,
275 pub identity: AgentProtocolRunIdentityV1,
276 pub command_digest: String,
277 pub state: AgentProtocolRunStateV1,
278 pub latest_event_sequence_exclusive: u64,
279 pub observed_at_ms: u64,
280 pub replayed: bool,
281}
282
283impl AgentProtocolCommandReceiptV1 {
284 pub const SCHEMA: &'static str = "a3s.code.agent-command-receipt.v1";
285
286 pub fn validate(&self) -> Result<(), AgentProtocolError> {
287 validate_schema(&self.schema, Self::SCHEMA)?;
288 validate_id("request_id", &self.request_id)?;
289 self.identity.validate()?;
290 validate_lower_sha256("command_digest", &self.command_digest)?;
291 if self.observed_at_ms == 0 {
292 return Err(AgentProtocolError::InvalidField("observed_at_ms"));
293 }
294 Ok(())
295 }
296
297 pub fn validate_for(&self, command: &AgentProtocolCommandV1) -> Result<(), AgentProtocolError> {
298 command.validate()?;
299 self.validate()?;
300 if self.action != command.action()
301 || self.request_id != command.request_id()
302 || self.identity != *command.identity()
303 || self.command_digest != command.digest()?
304 {
305 return Err(AgentProtocolError::IdentityMismatch);
306 }
307 if self.action == AgentProtocolCommandActionV1::Cancel && !self.state.is_terminal() {
308 return Err(AgentProtocolError::InvalidField("state"));
309 }
310 Ok(())
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct AgentProtocolEventRecordV1 {
318 pub sequence: u64,
319 pub occurred_at_ms: u64,
320 pub event: EventEnvelopeV1,
321}
322
323impl AgentProtocolEventRecordV1 {
324 pub fn from_run_event(
325 record: &RunEventRecord,
326 identity: &AgentProtocolRunIdentityV1,
327 ) -> Result<Self, AgentProtocolError> {
328 identity.validate()?;
329 let sequence = u64::try_from(record.sequence)
330 .map_err(|_| AgentProtocolError::InvalidField("sequence"))?;
331 let event = run_event_envelope_v1(record, &identity.run_id, &identity.session_id)
332 .map_err(|_| AgentProtocolError::Encoding)?;
333 let projected = Self {
334 sequence,
335 occurred_at_ms: record.timestamp_ms,
336 event,
337 };
338 projected.validate_for(identity)?;
339 Ok(projected)
340 }
341
342 pub fn validate_for(
347 &self,
348 identity: &AgentProtocolRunIdentityV1,
349 ) -> Result<(), AgentProtocolError> {
350 if self.event.version != EVENT_ENVELOPE_V1_VERSION {
351 return Err(AgentProtocolError::InvalidField("event.version"));
352 }
353 validate_single_line(
354 "event.type",
355 &self.event.event_type,
356 AGENT_PROTOCOL_MAX_EVENT_TYPE_BYTES,
357 )?;
358 validate_json_size(
359 "event.payload",
360 &self.event.payload,
361 AGENT_PROTOCOL_MAX_EVENT_PAYLOAD_BYTES,
362 )?;
363 let metadata = self
364 .event
365 .metadata
366 .as_ref()
367 .ok_or(AgentProtocolError::InvalidField("event.metadata"))?;
368 validate_json_size(
369 "event.metadata",
370 metadata,
371 AGENT_PROTOCOL_MAX_EVENT_METADATA_BYTES,
372 )?;
373 let metadata = metadata
374 .as_object()
375 .ok_or(AgentProtocolError::InvalidField("event.metadata"))?;
376 let exact = metadata.get("session_id").and_then(|value| value.as_str())
377 == Some(identity.session_id.as_str())
378 && metadata.get("run_id").and_then(|value| value.as_str())
379 == Some(identity.run_id.as_str())
380 && metadata.get("sequence").and_then(|value| value.as_u64()) == Some(self.sequence)
381 && metadata
382 .get("timestamp_ms")
383 .and_then(|value| value.as_u64())
384 == Some(self.occurred_at_ms);
385 if !exact {
386 return Err(AgentProtocolError::IdentityMismatch);
387 }
388 let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
389 if encoded.len() > AGENT_PROTOCOL_MAX_EVENT_RECORD_BYTES {
390 return Err(AgentProtocolError::InvalidField("event"));
391 }
392 Ok(())
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(deny_unknown_fields)]
399pub struct AgentProtocolEventPageRequestV1 {
400 pub schema: String,
401 pub identity: AgentProtocolRunIdentityV1,
402 pub after_event_sequence: Option<u64>,
403 pub limit: u16,
404}
405
406impl AgentProtocolEventPageRequestV1 {
407 pub const SCHEMA: &'static str = "a3s.code.agent-event-page-request.v1";
408
409 pub fn validate(&self) -> Result<(), AgentProtocolError> {
410 validate_schema(&self.schema, Self::SCHEMA)?;
411 self.identity.validate()?;
412 if self.limit == 0 || usize::from(self.limit) > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
413 return Err(AgentProtocolError::InvalidField("limit"));
414 }
415 Ok(())
416 }
417
418 pub fn digest(&self) -> Result<String, AgentProtocolError> {
419 digest_validated(self, || self.validate())
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425#[serde(deny_unknown_fields)]
426pub struct AgentProtocolEventPageV1 {
427 pub schema: String,
428 pub identity: AgentProtocolRunIdentityV1,
429 pub after_event_sequence: Option<u64>,
430 pub first_available_sequence: Option<u64>,
431 pub latest_sequence_exclusive: u64,
432 pub next_after_event_sequence: Option<u64>,
433 pub state: AgentProtocolRunStateV1,
434 pub observed_at_ms: u64,
435 pub retention_gap: bool,
436 pub has_more: bool,
437 pub events: Vec<AgentProtocolEventRecordV1>,
438}
439
440impl AgentProtocolEventPageV1 {
441 pub const SCHEMA: &'static str = "a3s.code.agent-event-page.v1";
442
443 pub fn from_run_page(
444 identity: AgentProtocolRunIdentityV1,
445 state: RunStatus,
446 observed_at_ms: u64,
447 after_event_sequence: Option<usize>,
448 page: &RunEventPage,
449 ) -> Result<Self, AgentProtocolError> {
450 identity.validate()?;
451 let convert = |value: usize| {
452 u64::try_from(value).map_err(|_| AgentProtocolError::InvalidField("sequence"))
453 };
454 let events = page
455 .events
456 .iter()
457 .map(|record| AgentProtocolEventRecordV1::from_run_event(record, &identity))
458 .collect::<Result<Vec<_>, _>>()?;
459 let projected = Self {
460 schema: Self::SCHEMA.into(),
461 identity,
462 after_event_sequence: after_event_sequence.map(convert).transpose()?,
463 first_available_sequence: page.first_available_sequence.map(convert).transpose()?,
464 latest_sequence_exclusive: convert(page.latest_sequence_exclusive)?,
465 next_after_event_sequence: page.next_after_sequence.map(convert).transpose()?,
466 state: state.into(),
467 observed_at_ms,
468 retention_gap: page.retention_gap,
469 has_more: page.has_more,
470 events,
471 };
472 projected.validate()?;
473 Ok(projected)
474 }
475
476 pub fn validate(&self) -> Result<(), AgentProtocolError> {
477 validate_schema(&self.schema, Self::SCHEMA)?;
478 self.identity.validate()?;
479 if self.events.len() > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
480 return Err(AgentProtocolError::InvalidField("events"));
481 }
482 if self
483 .first_available_sequence
484 .is_some_and(|sequence| sequence >= self.latest_sequence_exclusive)
485 {
486 return Err(AgentProtocolError::InvalidField("first_available_sequence"));
487 }
488
489 let requested_start = self
490 .after_event_sequence
491 .map(|sequence| sequence.saturating_add(1))
492 .unwrap_or(0);
493 let expected_gap = requested_start < self.latest_sequence_exclusive
494 && self
495 .first_available_sequence
496 .is_none_or(|first| requested_start < first);
497 if self.retention_gap != expected_gap {
498 return Err(AgentProtocolError::InvalidField("retention_gap"));
499 }
500
501 let mut previous: Option<(u64, u64)> = None;
502 for event in &self.events {
503 event.validate_for(&self.identity)?;
504 if event.occurred_at_ms > self.observed_at_ms
505 || previous.is_some_and(|(sequence, timestamp)| {
506 event.sequence != sequence.saturating_add(1) || event.occurred_at_ms < timestamp
507 })
508 || event.sequence >= self.latest_sequence_exclusive
509 {
510 return Err(AgentProtocolError::InvalidField("events"));
511 }
512 previous = Some((event.sequence, event.occurred_at_ms));
513 }
514
515 if let Some(first) = self.events.first() {
516 if (!self.retention_gap && first.sequence != requested_start)
517 || (self.retention_gap && self.first_available_sequence != Some(first.sequence))
518 || self
519 .first_available_sequence
520 .is_some_and(|available| first.sequence < available)
521 {
522 return Err(AgentProtocolError::InvalidField("events"));
523 }
524 } else if self.retention_gap && self.first_available_sequence.is_some() {
525 return Err(AgentProtocolError::InvalidField("events"));
526 }
527 let expected_next = self
528 .events
529 .last()
530 .map(|event| event.sequence)
531 .or(self.after_event_sequence);
532 if self.next_after_event_sequence != expected_next {
533 return Err(AgentProtocolError::InvalidField(
534 "next_after_event_sequence",
535 ));
536 }
537 if self.has_more {
538 if self.events.last().is_none_or(|event| {
539 event.sequence.saturating_add(1) >= self.latest_sequence_exclusive
540 }) {
541 return Err(AgentProtocolError::InvalidField("has_more"));
542 }
543 } else if let Some(last) = self.events.last() {
544 if last.sequence.saturating_add(1) != self.latest_sequence_exclusive {
545 return Err(AgentProtocolError::InvalidField("has_more"));
546 }
547 }
548 let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
549 if encoded.len() > AGENT_PROTOCOL_MAX_EVENT_PAGE_BYTES {
550 return Err(AgentProtocolError::InvalidField("events"));
551 }
552 Ok(())
553 }
554
555 pub fn first_sequence(&self) -> Option<u64> {
556 self.events.first().map(|event| event.sequence)
557 }
558
559 pub fn last_sequence(&self) -> Option<u64> {
560 self.events.last().map(|event| event.sequence)
561 }
562
563 pub fn digest(&self) -> Result<String, AgentProtocolError> {
564 digest_validated(self, || self.validate())
565 }
566}
567
568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(deny_unknown_fields)]
571pub struct AgentProtocolChangeSetRequestV1 {
572 pub schema: String,
573 pub identity: AgentProtocolRunIdentityV1,
574}
575
576impl AgentProtocolChangeSetRequestV1 {
577 pub const SCHEMA: &'static str = "a3s.code.agent-change-set-request.v1";
578
579 pub fn validate(&self) -> Result<(), AgentProtocolError> {
580 validate_schema(&self.schema, Self::SCHEMA)?;
581 self.identity.validate()
582 }
583
584 pub fn digest(&self) -> Result<String, AgentProtocolError> {
585 digest_validated(self, || self.validate())
586 }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(deny_unknown_fields)]
597pub struct AgentProtocolChangeSetV1 {
598 pub schema: String,
599 pub identity: AgentProtocolRunIdentityV1,
600 pub state: AgentProtocolRunStateV1,
601 pub format: String,
602 pub encoding: String,
603 pub base_tree: String,
604 pub result_tree: String,
605 pub patch_digest: String,
606 pub patch_bytes: u64,
607 pub patch_base64: String,
608 pub observed_at_ms: u64,
609}
610
611impl AgentProtocolChangeSetV1 {
612 pub const SCHEMA: &'static str = "a3s.code.agent-change-set.v1";
613
614 pub fn validate(&self) -> Result<(), AgentProtocolError> {
615 validate_schema(&self.schema, Self::SCHEMA)?;
616 self.identity.validate()?;
617 if !self.state.is_terminal() {
618 return Err(AgentProtocolError::InvalidField("state"));
619 }
620 if self.format != AGENT_PROTOCOL_CHANGE_SET_FORMAT_V1 {
621 return Err(AgentProtocolError::InvalidField("format"));
622 }
623 if self.encoding != AGENT_PROTOCOL_CHANGE_SET_ENCODING_V1 {
624 return Err(AgentProtocolError::InvalidField("encoding"));
625 }
626 validate_git_tree("base_tree", &self.base_tree)?;
627 validate_git_tree("result_tree", &self.result_tree)?;
628 validate_lower_sha256("patch_digest", &self.patch_digest)?;
629 let declared_bytes = usize::try_from(self.patch_bytes)
630 .map_err(|_| AgentProtocolError::InvalidField("patch_bytes"))?;
631 let patch = base64::engine::general_purpose::STANDARD
632 .decode(&self.patch_base64)
633 .map_err(|_| AgentProtocolError::InvalidField("patch_base64"))?;
634 if declared_bytes != patch.len()
635 || declared_bytes > AGENT_PROTOCOL_MAX_CHANGE_SET_BYTES
636 || self.patch_digest != format!("sha256:{:x}", Sha256::digest(&patch))
637 || self.observed_at_ms == 0
638 {
639 return Err(AgentProtocolError::InvalidField("patch_base64"));
640 }
641 let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
642 if encoded.len() > AGENT_PROTOCOL_MAX_CHANGE_SET_RESPONSE_BYTES {
643 return Err(AgentProtocolError::InvalidField("patch_base64"));
644 }
645 Ok(())
646 }
647
648 pub fn digest(&self) -> Result<String, AgentProtocolError> {
649 digest_validated(self, || self.validate())
650 }
651}
652
653fn validate_schema(value: &str, expected: &str) -> Result<(), AgentProtocolError> {
654 if value == expected {
655 Ok(())
656 } else {
657 Err(AgentProtocolError::UnsupportedSchema)
658 }
659}
660
661fn validate_id(field: &'static str, value: &str) -> Result<(), AgentProtocolError> {
662 validate_single_line(field, value, AGENT_PROTOCOL_MAX_ID_BYTES)
663}
664
665fn validate_single_line(
666 field: &'static str,
667 value: &str,
668 max: usize,
669) -> Result<(), AgentProtocolError> {
670 if value.trim().is_empty()
671 || value.len() > max
672 || value.contains('\0')
673 || value.contains(['\r', '\n'])
674 {
675 Err(AgentProtocolError::InvalidField(field))
676 } else {
677 Ok(())
678 }
679}
680
681pub(crate) fn validate_lower_sha256(
682 field: &'static str,
683 value: &str,
684) -> Result<(), AgentProtocolError> {
685 let valid = value.strip_prefix("sha256:").is_some_and(|hex| {
686 hex.len() == 64
687 && hex
688 .bytes()
689 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
690 });
691 if valid {
692 Ok(())
693 } else {
694 Err(AgentProtocolError::InvalidField(field))
695 }
696}
697
698fn validate_git_tree(field: &'static str, value: &str) -> Result<(), AgentProtocolError> {
699 let valid = value.strip_prefix("git-tree:").is_some_and(|hex| {
700 matches!(hex.len(), 40 | 64)
701 && hex
702 .bytes()
703 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
704 });
705 if valid {
706 Ok(())
707 } else {
708 Err(AgentProtocolError::InvalidField(field))
709 }
710}
711
712fn validate_json_size(
713 field: &'static str,
714 value: &serde_json::Value,
715 max: usize,
716) -> Result<(), AgentProtocolError> {
717 let encoded = serde_json::to_vec(value).map_err(|_| AgentProtocolError::Encoding)?;
718 if encoded.len() > max {
719 Err(AgentProtocolError::InvalidField(field))
720 } else {
721 Ok(())
722 }
723}
724
725fn digest_validated<T: Serialize>(
726 value: &T,
727 validate: impl FnOnce() -> Result<(), AgentProtocolError>,
728) -> Result<String, AgentProtocolError> {
729 validate()?;
730 let encoded = serde_json::to_vec(value).map_err(|_| AgentProtocolError::Encoding)?;
731 Ok(format!("sha256:{:x}", Sha256::digest(encoded)))
732}