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