1use 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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum ModelPresentationApplicationV1 {
84 Profiled,
85 Auxiliary,
86}
87
88#[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#[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#[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#[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#[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#[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 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}