Skip to main content

a3s_code_core/
harness_evidence.rs

1//! Bounded, content-addressed evidence for governed Tool requests,
2//! model-visible run capabilities, and provider-neutral model input.
3//!
4//! The snapshots in this module deliberately retain only counters, stable
5//! capability facts, and domain-separated digests. They never retain Tool
6//! arguments, prompt text, Tool output, source text, vectors, credentials, or
7//! provider endpoints.
8
9use crate::workspace::{WorkspaceCapabilities, WorkspaceRetrievalPhase};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13mod digest;
14mod input;
15mod source;
16#[cfg(test)]
17mod tests;
18mod tool_request;
19mod usage;
20
21use digest::{measure, require_digest, require_optional_digest};
22pub(crate) use source::{ModelCallObservation, RunCapabilityEvidenceSource};
23pub use tool_request::{
24    ToolRequestOriginV1, ToolRequestSnapshotV1, TOOL_REQUEST_SNAPSHOT_V1_SCHEMA,
25};
26pub(crate) use usage::ModelUsageBinding;
27pub use usage::{ModelUsageSnapshotV1, ToolResultContextUsageV1, MODEL_USAGE_SNAPSHOT_V1_SCHEMA};
28
29pub const RUN_CAPABILITY_SNAPSHOT_V1_SCHEMA: &str = "a3s.code.run-capability-snapshot.v1";
30pub const MODEL_PRESENTATION_SNAPSHOT_V1_SCHEMA: &str = "a3s.code.model-presentation-snapshot.v1";
31pub const MODEL_INPUT_SNAPSHOT_V1_SCHEMA: &str = "a3s.code.model-input-snapshot.v1";
32
33const CAPABILITY_SNAPSHOT_DOMAIN: &str = "a3s.code.run-capability-snapshot.v1";
34const MODEL_PRESENTATION_SNAPSHOT_DOMAIN: &str = "a3s.code.model-presentation-snapshot.v1";
35const MODEL_INPUT_SNAPSHOT_DOMAIN: &str = "a3s.code.model-input-snapshot.v1";
36const MODEL_INPUT_PAYLOAD_DOMAIN: &str = "a3s.code.model-input-payload.v1";
37const MODEL_MESSAGES_DOMAIN: &str = "a3s.code.model-input-messages.v1";
38const MODEL_SYSTEM_DOMAIN: &str = "a3s.code.model-input-system.v1";
39const MODEL_TOOLS_DOMAIN: &str = "a3s.code.model-visible-tools.v1";
40const MODEL_STRUCTURED_DOMAIN: &str = "a3s.code.model-input-structured.v1";
41const RETRIEVAL_RESULTS_DOMAIN: &str = "a3s.code.model-input-retrieval-results.v1";
42const TOOL_RESULT_CONTENT_DOMAIN: &str = "a3s.code.model-input-tool-result-content.v1";
43const TOOL_RESULT_CONTENTS_DOMAIN: &str = "a3s.code.model-input-tool-result-contents.v1";
44const REPEATED_TOOL_RESULT_CONTENTS_DOMAIN: &str =
45    "a3s.code.model-input-repeated-tool-result-contents.v1";
46const RETRIEVAL_MODEL_DOMAIN: &str = "a3s.code.workspace-retrieval-model.v1";
47const PERMISSION_POLICY_DOMAIN: &str = "a3s.code.permission-policy.v1";
48const CONFIRMATION_POLICY_DOMAIN: &str = "a3s.code.confirmation-policy.v1";
49
50#[derive(Debug, Error)]
51pub enum HarnessEvidenceError {
52    #[error("Harness evidence could not be serialized: {0}")]
53    Serialization(#[from] serde_json::Error),
54    #[error("Harness evidence uses an unsupported schema")]
55    UnsupportedSchema,
56    #[error("Harness evidence field `{0}` is not a canonical SHA-256 digest")]
57    InvalidDigest(&'static str),
58    #[error("Harness evidence violates invariant `{0}`")]
59    InvalidContents(&'static str),
60    #[error("Harness evidence field `{0}` does not match the snapshot contents")]
61    DigestMismatch(&'static str),
62    #[error("Harness model-call sequence is exhausted")]
63    CallSequenceExhausted,
64    #[error(transparent)]
65    ToolPresentation(#[from] crate::tools::ToolPresentationError),
66}
67
68/// Model-call shape used by one provider-neutral [`crate::llm::LlmClient`]
69/// invocation.
70#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum ModelInputKindV1 {
73    Completion,
74    Streaming,
75    Structured,
76    StreamingStructured,
77}
78
79/// Whether a provider-neutral call used the Session's Tool-presentation
80/// profile or a host-owned auxiliary protocol such as structured validation.
81#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum ModelPresentationApplicationV1 {
84    Profiled,
85    Auxiliary,
86}
87
88/// Non-sensitive workspace service surface visible to one model call.
89#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase", deny_unknown_fields)]
91pub struct WorkspaceCapabilitySnapshotV1 {
92    pub read: bool,
93    pub write: bool,
94    pub exec: bool,
95    pub search: bool,
96    pub git: bool,
97    pub code_intelligence: bool,
98}
99
100impl From<WorkspaceCapabilities> for WorkspaceCapabilitySnapshotV1 {
101    fn from(value: WorkspaceCapabilities) -> Self {
102        Self {
103            read: value.read,
104            write: value.write,
105            exec: value.exec,
106            search: value.search,
107            git: value.git,
108            code_intelligence: value.code_intelligence,
109        }
110    }
111}
112
113/// Run-owned governance bindings, serializable policy identities, and
114/// execution ceilings observed for the same run as the model call.
115#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct RunPolicyCeilingSnapshotV1 {
118    pub permission_checker_bound: bool,
119    pub permission_policy_digest: Option<String>,
120    pub confirmation_manager_bound: bool,
121    pub confirmation_policy_digest: Option<String>,
122    pub budget_guard_bound: bool,
123    pub active_skill_tool_restrictions: bool,
124    pub max_tool_rounds: usize,
125    pub max_parallel_tasks: usize,
126    pub tool_timeout_ms: Option<u64>,
127    pub llm_api_timeout_ms: Option<u64>,
128    pub max_execution_time_ms: Option<u64>,
129}
130
131/// Exact semantic readiness and generation identities observed immediately
132/// before a model call. Model identity is represented only by a digest of the
133/// non-sensitive provider descriptor.
134#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase", deny_unknown_fields)]
136pub struct WorkspaceRetrievalCapabilitySnapshotV1 {
137    pub enabled: bool,
138    pub phase: WorkspaceRetrievalPhase,
139    pub catalog_revision: u64,
140    pub source_revision: u64,
141    pub vector_revision: u64,
142    pub coverage_bps: u16,
143    pub model_digest: Option<String>,
144}
145
146/// Immutable description of the exact capability surface exposed for a model
147/// call. The digest excludes itself and remains stable while the surface and
148/// retrieval readiness generation remain unchanged.
149#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase", deny_unknown_fields)]
151pub struct RunCapabilitySnapshotV1 {
152    pub schema: String,
153    pub model_visible_tool_count: usize,
154    pub model_visible_tools_digest: String,
155    pub workspace: WorkspaceCapabilitySnapshotV1,
156    pub policy: RunPolicyCeilingSnapshotV1,
157    pub retrieval: WorkspaceRetrievalCapabilitySnapshotV1,
158    pub snapshot_digest: String,
159}
160
161impl RunCapabilitySnapshotV1 {
162    fn new(
163        model_visible_tool_count: usize,
164        model_visible_tools_digest: String,
165        workspace: WorkspaceCapabilitySnapshotV1,
166        policy: RunPolicyCeilingSnapshotV1,
167        retrieval: WorkspaceRetrievalCapabilitySnapshotV1,
168    ) -> Result<Self, HarnessEvidenceError> {
169        let mut snapshot = Self {
170            schema: RUN_CAPABILITY_SNAPSHOT_V1_SCHEMA.to_string(),
171            model_visible_tool_count,
172            model_visible_tools_digest,
173            workspace,
174            policy,
175            retrieval,
176            snapshot_digest: String::new(),
177        };
178        snapshot.snapshot_digest = snapshot.expected_digest()?;
179        Ok(snapshot)
180    }
181
182    pub fn validate(&self) -> Result<(), HarnessEvidenceError> {
183        if self.schema != RUN_CAPABILITY_SNAPSHOT_V1_SCHEMA {
184            return Err(HarnessEvidenceError::UnsupportedSchema);
185        }
186        let disabled_shape = self.retrieval.phase == WorkspaceRetrievalPhase::Disabled
187            && self.retrieval.catalog_revision == 0
188            && self.retrieval.source_revision == 0
189            && self.retrieval.vector_revision == 0
190            && self.retrieval.coverage_bps == 0
191            && self.retrieval.model_digest.is_none();
192        if self.retrieval.coverage_bps > 10_000 {
193            return Err(HarnessEvidenceError::InvalidContents(
194                "retrieval.coverage_bps <= 10_000",
195            ));
196        }
197        if self.retrieval.enabled && self.retrieval.phase == WorkspaceRetrievalPhase::Disabled {
198            return Err(HarnessEvidenceError::InvalidContents(
199                "enabled retrieval has a live phase",
200            ));
201        }
202        if self.retrieval.enabled && self.retrieval.model_digest.is_none() {
203            return Err(HarnessEvidenceError::InvalidContents(
204                "enabled retrieval has a model descriptor digest",
205            ));
206        }
207        if !self.retrieval.enabled && !disabled_shape {
208            return Err(HarnessEvidenceError::InvalidContents(
209                "disabled retrieval has an empty generation",
210            ));
211        }
212        require_digest(
213            "model_visible_tools_digest",
214            &self.model_visible_tools_digest,
215        )?;
216        require_optional_digest(
217            "permission_policy_digest",
218            self.policy.permission_policy_digest.as_deref(),
219        )?;
220        require_optional_digest(
221            "confirmation_policy_digest",
222            self.policy.confirmation_policy_digest.as_deref(),
223        )?;
224        require_optional_digest(
225            "retrieval.model_digest",
226            self.retrieval.model_digest.as_deref(),
227        )?;
228        require_digest("snapshot_digest", &self.snapshot_digest)?;
229        if self.snapshot_digest != self.expected_digest()? {
230            return Err(HarnessEvidenceError::DigestMismatch("snapshot_digest"));
231        }
232        Ok(())
233    }
234
235    fn expected_digest(&self) -> Result<String, HarnessEvidenceError> {
236        #[derive(Serialize)]
237        struct Identity<'a> {
238            schema: &'a str,
239            model_visible_tool_count: usize,
240            model_visible_tools_digest: &'a str,
241            workspace: &'a WorkspaceCapabilitySnapshotV1,
242            policy: &'a RunPolicyCeilingSnapshotV1,
243            retrieval: &'a WorkspaceRetrievalCapabilitySnapshotV1,
244        }
245
246        Ok(measure(
247            CAPABILITY_SNAPSHOT_DOMAIN,
248            &Identity {
249                schema: &self.schema,
250                model_visible_tool_count: self.model_visible_tool_count,
251                model_visible_tools_digest: &self.model_visible_tools_digest,
252                workspace: &self.workspace,
253                policy: &self.policy,
254                retrieval: &self.retrieval,
255            },
256        )?
257        .digest)
258    }
259}
260
261/// Per-call evidence binding a frozen presentation-profile identity to both
262/// its canonical source definitions and the definitions actually submitted to
263/// the provider-neutral LLM boundary.
264#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase", deny_unknown_fields)]
266pub struct ModelPresentationSnapshotV1 {
267    pub schema: String,
268    pub call_sequence: u64,
269    pub profile: crate::tools::ToolPresentationProfileV1,
270    pub application: ModelPresentationApplicationV1,
271    pub source_tool_count: usize,
272    pub source_tool_definitions_digest: String,
273    pub source_estimated_tokens: usize,
274    pub presented_tool_count: usize,
275    pub presented_tool_definitions_digest: String,
276    pub presented_estimated_tokens: usize,
277    pub snapshot_digest: String,
278}
279
280impl ModelPresentationSnapshotV1 {
281    pub(crate) fn new(
282        call_sequence: u64,
283        profile: crate::tools::ToolPresentationProfileV1,
284        application: ModelPresentationApplicationV1,
285        source_tools: &[crate::llm::ToolDefinition],
286        source_tool_definitions_digest: String,
287        presented_tools: &[crate::llm::ToolDefinition],
288        presented_tool_definitions_digest: String,
289    ) -> Result<Self, HarnessEvidenceError> {
290        let mut snapshot = Self {
291            schema: MODEL_PRESENTATION_SNAPSHOT_V1_SCHEMA.to_owned(),
292            call_sequence,
293            profile,
294            application,
295            source_tool_count: source_tools.len(),
296            source_tool_definitions_digest,
297            source_estimated_tokens: crate::tools::estimated_definition_tokens(source_tools),
298            presented_tool_count: presented_tools.len(),
299            presented_tool_definitions_digest,
300            presented_estimated_tokens: crate::tools::estimated_definition_tokens(presented_tools),
301            snapshot_digest: String::new(),
302        };
303        snapshot.snapshot_digest = snapshot.expected_digest()?;
304        snapshot.validate()?;
305        Ok(snapshot)
306    }
307
308    pub fn validate(&self) -> Result<(), HarnessEvidenceError> {
309        if self.schema != MODEL_PRESENTATION_SNAPSHOT_V1_SCHEMA {
310            return Err(HarnessEvidenceError::UnsupportedSchema);
311        }
312        if self.call_sequence == 0 {
313            return Err(HarnessEvidenceError::InvalidContents(
314                "presentation call_sequence is positive",
315            ));
316        }
317        self.profile.validate()?;
318        require_digest(
319            "source_tool_definitions_digest",
320            &self.source_tool_definitions_digest,
321        )?;
322        require_digest(
323            "presented_tool_definitions_digest",
324            &self.presented_tool_definitions_digest,
325        )?;
326        require_digest("snapshot_digest", &self.snapshot_digest)?;
327        if self.application == ModelPresentationApplicationV1::Profiled
328            && self.presented_tool_count > self.source_tool_count
329        {
330            return Err(HarnessEvidenceError::InvalidContents(
331                "profiled presentation cannot add Tool definitions",
332            ));
333        }
334        if self.application == ModelPresentationApplicationV1::Auxiliary
335            && (self.source_tool_count != self.presented_tool_count
336                || self.source_tool_definitions_digest != self.presented_tool_definitions_digest
337                || self.source_estimated_tokens != self.presented_estimated_tokens)
338        {
339            return Err(HarnessEvidenceError::InvalidContents(
340                "auxiliary presentation source and submitted definitions agree",
341            ));
342        }
343        if self.snapshot_digest != self.expected_digest()? {
344            return Err(HarnessEvidenceError::DigestMismatch("snapshot_digest"));
345        }
346        Ok(())
347    }
348
349    pub fn validate_against(
350        &self,
351        input: &ModelInputSnapshotV1,
352    ) -> Result<(), HarnessEvidenceError> {
353        self.validate()?;
354        input.validate()?;
355        if self.call_sequence != input.call_sequence {
356            return Err(HarnessEvidenceError::InvalidContents(
357                "presentation and input call sequences agree",
358            ));
359        }
360        if self.presented_tool_count != input.tool_count {
361            return Err(HarnessEvidenceError::InvalidContents(
362                "presentation and input Tool counts agree",
363            ));
364        }
365        if self.presented_tool_definitions_digest != input.tool_definitions_digest {
366            return Err(HarnessEvidenceError::DigestMismatch(
367                "presented_tool_definitions_digest",
368            ));
369        }
370        Ok(())
371    }
372
373    fn expected_digest(&self) -> Result<String, HarnessEvidenceError> {
374        #[derive(Serialize)]
375        struct Identity<'a> {
376            schema: &'a str,
377            call_sequence: u64,
378            profile: &'a crate::tools::ToolPresentationProfileV1,
379            application: ModelPresentationApplicationV1,
380            source_tool_count: usize,
381            source_tool_definitions_digest: &'a str,
382            source_estimated_tokens: usize,
383            presented_tool_count: usize,
384            presented_tool_definitions_digest: &'a str,
385            presented_estimated_tokens: usize,
386        }
387
388        Ok(measure(
389            MODEL_PRESENTATION_SNAPSHOT_DOMAIN,
390            &Identity {
391                schema: &self.schema,
392                call_sequence: self.call_sequence,
393                profile: &self.profile,
394                application: self.application,
395                source_tool_count: self.source_tool_count,
396                source_tool_definitions_digest: &self.source_tool_definitions_digest,
397                source_estimated_tokens: self.source_estimated_tokens,
398                presented_tool_count: self.presented_tool_count,
399                presented_tool_definitions_digest: &self.presented_tool_definitions_digest,
400                presented_estimated_tokens: self.presented_estimated_tokens,
401            },
402        )?
403        .digest)
404    }
405}
406
407/// Immutable, bounded evidence for the arguments submitted to one
408/// provider-neutral model call.
409#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase", deny_unknown_fields)]
411pub struct ModelInputSnapshotV1 {
412    pub schema: String,
413    pub call_sequence: u64,
414    pub kind: ModelInputKindV1,
415    pub message_count: usize,
416    pub content_block_count: usize,
417    pub image_block_count: usize,
418    pub tool_result_count: usize,
419    pub tool_count: usize,
420    pub retrieval_result_count: usize,
421    pub retrieval_result_bytes: u64,
422    pub retrieval_results_digest: Option<String>,
423    pub system_bytes: u64,
424    pub message_payload_bytes: u64,
425    pub tool_definition_bytes: u64,
426    pub structured_output_bytes: u64,
427    pub payload_bytes: u64,
428    pub estimated_prompt_tokens: usize,
429    pub messages_digest: String,
430    pub system_digest: Option<String>,
431    pub tool_definitions_digest: String,
432    pub structured_output_digest: Option<String>,
433    pub input_digest: String,
434    pub capability_snapshot_digest: String,
435    pub snapshot_digest: String,
436}
437
438impl ModelInputSnapshotV1 {
439    pub fn validate(&self) -> Result<(), HarnessEvidenceError> {
440        if self.schema != MODEL_INPUT_SNAPSHOT_V1_SCHEMA {
441            return Err(HarnessEvidenceError::UnsupportedSchema);
442        }
443        if self.call_sequence == 0 {
444            return Err(HarnessEvidenceError::InvalidContents(
445                "call_sequence is positive",
446            ));
447        }
448        if self.tool_result_count > self.content_block_count {
449            return Err(HarnessEvidenceError::InvalidContents(
450                "tool results are content blocks",
451            ));
452        }
453        if self.retrieval_result_count > self.tool_result_count {
454            return Err(HarnessEvidenceError::InvalidContents(
455                "retrieval results are tool results",
456            ));
457        }
458        if (self.retrieval_result_count == 0) != self.retrieval_results_digest.is_none()
459            || (self.retrieval_result_count == 0) != (self.retrieval_result_bytes == 0)
460        {
461            return Err(HarnessEvidenceError::InvalidContents(
462                "retrieval result count, bytes, and digest agree",
463            ));
464        }
465        if (self.system_bytes == 0) != self.system_digest.is_none() {
466            return Err(HarnessEvidenceError::InvalidContents(
467                "system bytes and digest agree",
468            ));
469        }
470        if self.structured_output_digest.is_none() && self.structured_output_bytes != 0 {
471            return Err(HarnessEvidenceError::InvalidContents(
472                "structured-output bytes require a directive digest",
473            ));
474        }
475        if self.structured_output_digest.is_some() && self.structured_output_bytes == 0 {
476            return Err(HarnessEvidenceError::InvalidContents(
477                "a directive digest has serialized bytes",
478            ));
479        }
480        if self.message_payload_bytes == 0
481            || self.tool_definition_bytes == 0
482            || self.payload_bytes == 0
483        {
484            return Err(HarnessEvidenceError::InvalidContents(
485                "serialized input components are non-empty",
486            ));
487        }
488        for (field, digest) in [
489            ("messages_digest", self.messages_digest.as_str()),
490            (
491                "tool_definitions_digest",
492                self.tool_definitions_digest.as_str(),
493            ),
494            ("input_digest", self.input_digest.as_str()),
495            (
496                "capability_snapshot_digest",
497                self.capability_snapshot_digest.as_str(),
498            ),
499            ("snapshot_digest", self.snapshot_digest.as_str()),
500        ] {
501            require_digest(field, digest)?;
502        }
503        require_optional_digest("system_digest", self.system_digest.as_deref())?;
504        require_optional_digest(
505            "structured_output_digest",
506            self.structured_output_digest.as_deref(),
507        )?;
508        require_optional_digest(
509            "retrieval_results_digest",
510            self.retrieval_results_digest.as_deref(),
511        )?;
512        if self.snapshot_digest != self.expected_digest()? {
513            return Err(HarnessEvidenceError::DigestMismatch("snapshot_digest"));
514        }
515        Ok(())
516    }
517
518    /// Validate this input and the exact capability snapshot it references.
519    pub fn validate_against(
520        &self,
521        capability: &RunCapabilitySnapshotV1,
522    ) -> Result<(), HarnessEvidenceError> {
523        self.validate()?;
524        capability.validate()?;
525        if self.capability_snapshot_digest != capability.snapshot_digest {
526            return Err(HarnessEvidenceError::DigestMismatch(
527                "capability_snapshot_digest",
528            ));
529        }
530        if self.tool_count != capability.model_visible_tool_count {
531            return Err(HarnessEvidenceError::InvalidContents(
532                "input and capability tool counts agree",
533            ));
534        }
535        if self.tool_definitions_digest != capability.model_visible_tools_digest {
536            return Err(HarnessEvidenceError::DigestMismatch(
537                "tool_definitions_digest",
538            ));
539        }
540        Ok(())
541    }
542
543    fn expected_digest(&self) -> Result<String, HarnessEvidenceError> {
544        #[derive(Serialize)]
545        struct Identity<'a> {
546            schema: &'a str,
547            call_sequence: u64,
548            kind: ModelInputKindV1,
549            message_count: usize,
550            content_block_count: usize,
551            image_block_count: usize,
552            tool_result_count: usize,
553            tool_count: usize,
554            retrieval_result_count: usize,
555            retrieval_result_bytes: u64,
556            retrieval_results_digest: &'a Option<String>,
557            system_bytes: u64,
558            message_payload_bytes: u64,
559            tool_definition_bytes: u64,
560            structured_output_bytes: u64,
561            payload_bytes: u64,
562            estimated_prompt_tokens: usize,
563            messages_digest: &'a str,
564            system_digest: &'a Option<String>,
565            tool_definitions_digest: &'a str,
566            structured_output_digest: &'a Option<String>,
567            input_digest: &'a str,
568            capability_snapshot_digest: &'a str,
569        }
570
571        Ok(measure(
572            MODEL_INPUT_SNAPSHOT_DOMAIN,
573            &Identity {
574                schema: &self.schema,
575                call_sequence: self.call_sequence,
576                kind: self.kind,
577                message_count: self.message_count,
578                content_block_count: self.content_block_count,
579                image_block_count: self.image_block_count,
580                tool_result_count: self.tool_result_count,
581                tool_count: self.tool_count,
582                retrieval_result_count: self.retrieval_result_count,
583                retrieval_result_bytes: self.retrieval_result_bytes,
584                retrieval_results_digest: &self.retrieval_results_digest,
585                system_bytes: self.system_bytes,
586                message_payload_bytes: self.message_payload_bytes,
587                tool_definition_bytes: self.tool_definition_bytes,
588                structured_output_bytes: self.structured_output_bytes,
589                payload_bytes: self.payload_bytes,
590                estimated_prompt_tokens: self.estimated_prompt_tokens,
591                messages_digest: &self.messages_digest,
592                system_digest: &self.system_digest,
593                tool_definitions_digest: &self.tool_definitions_digest,
594                structured_output_digest: &self.structured_output_digest,
595                input_digest: &self.input_digest,
596                capability_snapshot_digest: &self.capability_snapshot_digest,
597            },
598        )?
599        .digest)
600    }
601}