1use std::collections::{BTreeMap, BTreeSet};
70
71use schemars::{schema::RootSchema, schema_for, JsonSchema};
72use serde::Serialize;
73use serde_json::{json, Value};
74use sha2::{Digest, Sha256};
75
76pub const FORMAT: &str = "car.wire-schema.v1";
77pub const DIGEST_ALGORITHM: &str = "sha256";
78pub const FOLLOW_UP_BEAD: &str = "car-86cq.1";
79
80const COMMITTED_SCHEMA: &str = include_str!("../wire-schema.json");
95const COMMITTED_DIGEST: &str = include_str!("../wire-schema.sha256");
96
97#[derive(Serialize, JsonSchema)]
100pub(crate) struct ServerHandshakeResult {
101 pub protocol_version: u32,
102 pub server_version: String,
103 pub client_protocol_version: u64,
104 pub client_version: String,
105 pub negotiated_capabilities: Vec<String>,
106 pub assistant_name: String,
107 pub assistant_aliases: Vec<String>,
108 pub assistant_brand: String,
109}
110
111#[derive(Serialize, JsonSchema)]
113pub(crate) struct ToolsListResult {
114 pub tools: Vec<car_ir::ToolSchema>,
115 pub count: usize,
116}
117
118#[derive(Serialize, JsonSchema)]
120#[serde(transparent)]
121pub(crate) struct ToolsRegisterResult(pub usize);
122
123#[derive(Serialize, JsonSchema)]
125pub(crate) struct ToolsUnregisterResult {
126 pub unregistered: String,
127 pub removed: u32,
128}
129
130#[derive(Serialize, JsonSchema)]
132pub(crate) struct ToolsCancelResult {
133 pub cancelled: bool,
134}
135
136#[derive(Serialize, JsonSchema)]
138pub(crate) struct ToolsStreamSubscribeResult {
139 pub subscribed: bool,
140}
141
142#[derive(Serialize, JsonSchema)]
144#[serde(rename_all = "lowercase")]
145pub(crate) enum StateSetResult {
146 Ok,
147}
148
149#[derive(Serialize, JsonSchema)]
151#[serde(transparent)]
152pub(crate) struct StateExistsResult(pub bool);
153
154#[derive(Serialize, JsonSchema)]
156#[serde(transparent)]
157pub(crate) struct StateKeysResult(pub Vec<String>);
158
159#[derive(Serialize, JsonSchema)]
161#[serde(transparent)]
162pub(crate) struct StateSnapshotResult(
163 #[schemars(with = "BTreeMap<String, Value>")] pub serde_json::Map<String, Value>,
164);
165
166#[derive(Clone, Copy, Serialize, JsonSchema)]
168#[serde(rename_all = "snake_case")]
169pub(crate) enum CapabilityRole {
170 Agent,
171 Owner,
172 Operator,
173 Host,
174}
175
176impl CapabilityRole {
177 pub(crate) fn from_manifest(role: &str) -> Self {
178 match role {
179 "agent" => Self::Agent,
180 "owner" => Self::Owner,
181 "operator" => Self::Operator,
182 "host" => Self::Host,
183 other => panic!("generated RPC capability has unknown role `{other}`"),
184 }
185 }
186}
187
188#[derive(Serialize, JsonSchema)]
190pub(crate) struct CapabilityMethodRow {
191 pub method: String,
192 pub role: CapabilityRole,
193}
194
195#[derive(Serialize, JsonSchema)]
197pub(crate) struct CapabilitiesListResult {
198 pub caller_role: CapabilityRole,
199 pub count: usize,
200 pub methods: Vec<CapabilityMethodRow>,
201}
202
203#[derive(Serialize, JsonSchema)]
205pub(crate) struct ServerSchemaResult {
206 pub schema: Value,
207 pub digest: String,
208 pub digest_algorithm: DigestAlgorithm,
209 pub car_version: String,
212}
213
214#[derive(Serialize, JsonSchema)]
215#[serde(rename_all = "lowercase")]
216pub(crate) enum DigestAlgorithm {
217 Sha256,
218}
219
220#[derive(Serialize, JsonSchema)]
227pub(crate) struct ManagedAgentWire {
228 pub id: String,
229 pub name: String,
230 pub command: String,
231 pub args: Vec<String>,
232 pub cwd: Option<String>,
233 pub env: BTreeMap<String, String>,
234 pub restart: car_registry::supervisor::RestartPolicy,
235 pub max_restarts: u32,
236 pub backoff_secs: u64,
237 pub auto_start: bool,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub method_allowlist: Option<Vec<String>>,
240 pub capabilities: Vec<String>,
241 pub status: car_registry::supervisor::AgentStatus,
242 pub pid: Option<u32>,
243 pub last_exit_code: Option<i32>,
244 pub restart_count: u32,
245 pub started_at: Option<i64>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub blocked_by_pid: Option<i32>,
248}
249
250impl ManagedAgentWire {
251 pub(crate) fn from_managed(agent: &car_registry::supervisor::ManagedAgent) -> Self {
252 let spec = &agent.spec;
253 Self {
254 id: spec.id.clone(),
255 name: spec.name.clone(),
256 command: spec.command.clone(),
257 args: spec.args.clone(),
258 cwd: spec
259 .cwd
260 .as_ref()
261 .map(|path| path.to_string_lossy().into_owned()),
262 env: spec.env.clone(),
263 restart: spec.restart,
264 max_restarts: spec.max_restarts,
265 backoff_secs: spec.backoff_secs,
266 auto_start: spec.auto_start,
267 method_allowlist: spec.method_allowlist.clone(),
268 capabilities: spec.capabilities.clone(),
269 status: agent.status,
270 pid: agent.pid,
271 last_exit_code: agent.last_exit_code,
272 restart_count: agent.restart_count,
273 started_at: agent.started_at,
274 blocked_by_pid: agent.blocked_by_pid,
275 }
276 }
277}
278
279#[derive(Serialize, JsonSchema)]
282pub(crate) struct ManagedAgentListRow {
283 #[serde(flatten)]
284 pub agent: ManagedAgentWire,
285 pub attached: bool,
288 #[serde(skip_serializing_if = "Option::is_none")]
292 pub tools: Option<Vec<String>>,
293 pub manifest_path: String,
294 pub log_path: String,
295 pub stderr_log_path: String,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub session_id: Option<String>,
298}
299
300#[derive(Serialize, JsonSchema)]
303pub(crate) struct DeclarativeAgentRow {
304 pub id: String,
305 pub name: String,
306 pub kind: DeclarativeAgentKind,
307 pub enabled: bool,
308 pub capabilities: Vec<String>,
309 pub description: String,
310 pub tools: Vec<String>,
311 pub goal: Option<car_registry::declarative::DeclarativeGoal>,
312 pub scenarios: usize,
313}
314
315impl DeclarativeAgentRow {
316 pub(crate) fn from_spec(spec: &car_registry::declarative::DeclarativeAgentSpec) -> Self {
317 let description = if spec.standing_goal.trim().is_empty() {
318 spec.identity.trim()
319 } else {
320 spec.standing_goal.trim()
321 };
322 Self {
323 id: spec.id.clone(),
324 name: spec.name.clone(),
325 kind: DeclarativeAgentKind::Declarative,
326 enabled: spec.enabled,
327 capabilities: vec!["chat".to_string()],
328 description: description.to_string(),
329 tools: spec.tools.clone(),
330 goal: spec.goal.clone(),
331 scenarios: spec.scenarios.len(),
332 }
333 }
334}
335
336#[derive(Serialize, JsonSchema)]
337#[serde(rename_all = "snake_case")]
338pub(crate) enum DeclarativeAgentKind {
339 Declarative,
340}
341
342#[allow(dead_code)]
347#[derive(Serialize, JsonSchema)]
348#[serde(untagged)]
349pub(crate) enum CarInspectResult {
350 Managed(ManagedAgentListRow),
351 Declarative(DeclarativeAgentRow),
352}
353
354fn closed_schema<T: JsonSchema>() -> Value {
355 let root: RootSchema = schema_for!(T);
356 let mut value = serde_json::to_value(root).expect("RootSchema serialization is infallible");
357 close_declared_objects(&mut value);
358 value
359}
360
361fn rpc_method_inventory() -> BTreeSet<&'static str> {
364 let mut methods = BTreeSet::from(["server.handshake"]);
365 for (method, _) in crate::generated_rpc_capabilities::RPC_CAPABILITIES {
366 assert!(
367 methods.insert(method),
368 "duplicate daemon RPC method in source inventory: {method}"
369 );
370 }
371 methods
372}
373
374fn insert_rpc_schema(
375 schemas: &mut BTreeMap<String, Value>,
376 covered_rpc_methods: &mut BTreeSet<&'static str>,
377 method: &'static str,
378 schema: Value,
379) {
380 assert!(
381 rpc_method_inventory().contains(method),
382 "wire schema covers unknown daemon RPC method: {method}"
383 );
384 assert!(
385 covered_rpc_methods.insert(method),
386 "wire schema covers daemon RPC method twice: {method}"
387 );
388 let key = format!("rpc.{method}.result");
389 assert!(
390 schemas.insert(key.clone(), schema).is_none(),
391 "duplicate wire schema key: {key}"
392 );
393}
394
395fn event_kind_inventory(event_schema: &Value) -> BTreeSet<String> {
399 fn collect(value: &Value, out: &mut BTreeSet<String>) {
400 match value {
401 Value::Array(values) => {
402 for value in values {
403 collect(value, out);
404 }
405 }
406 Value::Object(map) => {
407 if let Some(values) = map.get("enum").and_then(Value::as_array) {
408 for value in values {
409 if let Some(value) = value.as_str() {
410 out.insert(value.to_string());
411 }
412 }
413 }
414 for value in map.values() {
415 collect(value, out);
416 }
417 }
418 _ => {}
419 }
420 }
421
422 let mut kinds = BTreeSet::new();
423 collect(&event_schema["definitions"]["EventKind"], &mut kinds);
424 assert!(!kinds.is_empty(), "generated EventKind inventory is empty");
425 kinds
426}
427
428fn require_fields(schema: &mut Value, fields: &[&str]) {
429 schema["required"] = Value::Array(
430 fields
431 .iter()
432 .map(|field| Value::String((*field).to_string()))
433 .collect(),
434 );
435}
436
437fn require_all_declared_fields(value: &mut Value) {
448 match value {
449 Value::Array(values) => {
450 for value in values {
451 require_all_declared_fields(value);
452 }
453 }
454 Value::Object(map) => {
455 for value in map.values_mut() {
456 require_all_declared_fields(value);
457 }
458 if let Some(Value::Object(properties)) = map.get("properties") {
459 let fields = properties.keys().cloned().map(Value::String).collect();
460 map.insert("required".into(), Value::Array(fields));
461 }
462 }
463 _ => {}
464 }
465}
466
467fn require_fields_for(schema: &mut Value, title: &str, fields: &[&str]) {
478 fn walk(
479 value: &mut Value,
480 title: &str,
481 fields: &[&str],
482 applied: &mut bool,
483 own_name: Option<&str>,
484 entries_are_definitions: bool,
485 ) {
486 match value {
487 Value::Array(values) => {
488 for value in values {
489 walk(value, title, fields, applied, None, false);
490 }
491 }
492 Value::Object(map) => {
493 let named = own_name == Some(title)
494 || map.get("title").and_then(Value::as_str) == Some(title);
495 if named && map.contains_key("properties") {
496 let required = fields
497 .iter()
498 .map(|field| Value::String((*field).to_string()))
499 .collect();
500 map.insert("required".into(), Value::Array(required));
501 *applied = true;
502 }
503 for (key, child) in map.iter_mut() {
504 let child_name = entries_are_definitions.then(|| key.clone());
505 let child_defines = !entries_are_definitions && key == "definitions";
506 walk(
507 child,
508 title,
509 fields,
510 applied,
511 child_name.as_deref(),
512 child_defines,
513 );
514 }
515 }
516 _ => {}
517 }
518 }
519
520 let mut applied = false;
521 walk(schema, title, fields, &mut applied, None, false);
522 assert!(
523 applied,
524 "generated schema has no object named {title} to require fields on"
525 );
526}
527
528fn close_declared_objects(value: &mut Value) {
533 match value {
534 Value::Array(values) => {
535 for value in values {
536 close_declared_objects(value);
537 }
538 }
539 Value::Object(map) => {
540 for value in map.values_mut() {
541 close_declared_objects(value);
542 }
543 map.remove("default");
547 if map.contains_key("properties") && !map.contains_key("additionalProperties") {
548 map.insert("additionalProperties".into(), Value::Bool(false));
549 }
550 }
551 _ => {}
552 }
553}
554
555pub fn document() -> Value {
562 let mut schemas = BTreeMap::new();
563 let mut covered_rpc_methods = BTreeSet::new();
564
565 let mut inspect = closed_schema::<CarInspectResult>();
566 require_fields_for(
567 &mut inspect,
568 "ManagedAgentListRow",
569 MANAGED_AGENT_LIST_ROW_REQUIRED,
570 );
571 require_fields_for(
572 &mut inspect,
573 "DeclarativeAgentRow",
574 DECLARATIVE_AGENT_ROW_REQUIRED,
575 );
576 require_fields_for(&mut inspect, "DeclarativeGoal", DECLARATIVE_GOAL_REQUIRED);
577 schemas.insert("cli.car_inspect.result".to_string(), inspect);
578
579 let mut event = closed_schema::<car_eventlog::Event>();
580 require_fields(&mut event, EVENT_REQUIRED);
581 let all_event_kinds = event_kind_inventory(&event);
582 schemas.insert("journal.event".to_string(), event);
583
584 insert_rpc_schema(
585 &mut schemas,
586 &mut covered_rpc_methods,
587 "capabilities.list",
588 closed_schema::<CapabilitiesListResult>(),
589 );
590
591 let mut inference = closed_schema::<car_inference::InferenceResult>();
592 require_fields(&mut inference, INFERENCE_RESULT_REQUIRED);
593 require_fields_for(&mut inference, "TokenUsage", TOKEN_USAGE_REQUIRED);
594 require_fields_for(&mut inference, "ToolCall", TOOL_CALL_REQUIRED);
595 require_fields_for(&mut inference, "ThinkingBlock", THINKING_BLOCK_REQUIRED);
596 require_fields_for(&mut inference, "BoundingBox", BOUNDING_BOX_REQUIRED);
597 require_fields_for(&mut inference, "FallbackFrom", FALLBACK_FROM_REQUIRED);
598 insert_rpc_schema(&mut schemas, &mut covered_rpc_methods, "infer", inference);
599
600 let mut catalog = closed_schema::<car_inference::catalog_identity::CatalogSnapshot>();
601 require_all_declared_fields(&mut catalog);
602 require_fields_for(
606 &mut catalog,
607 "QuantizationObjectWireSchema",
608 QUANTIZATION_OBJECT_REQUIRED,
609 );
610 insert_rpc_schema(
611 &mut schemas,
612 &mut covered_rpc_methods,
613 "models.catalog_snapshot",
614 catalog,
615 );
616
617 insert_rpc_schema(
620 &mut schemas,
621 &mut covered_rpc_methods,
622 "server.handshake",
623 closed_schema::<ServerHandshakeResult>(),
624 );
625 insert_rpc_schema(
626 &mut schemas,
627 &mut covered_rpc_methods,
628 "server.schema",
629 closed_schema::<ServerSchemaResult>(),
630 );
631
632 insert_rpc_schema(
633 &mut schemas,
634 &mut covered_rpc_methods,
635 "state.get",
636 closed_schema::<Value>(),
637 );
638 insert_rpc_schema(
639 &mut schemas,
640 &mut covered_rpc_methods,
641 "state.set",
642 closed_schema::<StateSetResult>(),
643 );
644 insert_rpc_schema(
645 &mut schemas,
646 &mut covered_rpc_methods,
647 "state.exists",
648 closed_schema::<StateExistsResult>(),
649 );
650 insert_rpc_schema(
651 &mut schemas,
652 &mut covered_rpc_methods,
653 "state.keys",
654 closed_schema::<StateKeysResult>(),
655 );
656 insert_rpc_schema(
657 &mut schemas,
658 &mut covered_rpc_methods,
659 "state.snapshot",
660 closed_schema::<StateSnapshotResult>(),
661 );
662
663 insert_rpc_schema(
664 &mut schemas,
665 &mut covered_rpc_methods,
666 "tools.register",
667 closed_schema::<ToolsRegisterResult>(),
668 );
669 let mut tools = closed_schema::<ToolsListResult>();
670 require_fields_for(&mut tools, "ToolSchema", TOOL_SCHEMA_REQUIRED);
671 insert_rpc_schema(&mut schemas, &mut covered_rpc_methods, "tools.list", tools);
672 insert_rpc_schema(
673 &mut schemas,
674 &mut covered_rpc_methods,
675 "tools.unregister",
676 closed_schema::<ToolsUnregisterResult>(),
677 );
678 let mut poll = closed_schema::<Option<car_engine::tool_handles::ToolPollResult>>();
679 require_fields_for(&mut poll, "ToolPollResult", TOOL_POLL_RESULT_REQUIRED);
680 insert_rpc_schema(&mut schemas, &mut covered_rpc_methods, "tools.poll", poll);
681 insert_rpc_schema(
682 &mut schemas,
683 &mut covered_rpc_methods,
684 "tools.cancel",
685 closed_schema::<ToolsCancelResult>(),
686 );
687 insert_rpc_schema(
688 &mut schemas,
689 &mut covered_rpc_methods,
690 "tools.stream.subscribe",
691 closed_schema::<ToolsStreamSubscribeResult>(),
692 );
693
694 let mut action_result = closed_schema::<car_ir::ActionResult>();
695 require_fields(&mut action_result, ACTION_RESULT_REQUIRED);
696 schemas.insert("type.action_result".to_string(), action_result);
697
698 let all_rpc_methods = rpc_method_inventory();
699 let uncovered_rpc_methods: Vec<&str> = all_rpc_methods
700 .difference(&covered_rpc_methods)
701 .copied()
702 .collect();
703 let covered_event_kinds: BTreeSet<String> = BTreeSet::new();
704 let uncovered_event_kinds: Vec<&str> = all_event_kinds
705 .difference(&covered_event_kinds)
706 .map(String::as_str)
707 .collect();
708 let complete = uncovered_rpc_methods.is_empty() && uncovered_event_kinds.is_empty();
709
710 json!({
711 "format": FORMAT,
712 "json_schema_draft": "http://json-schema.org/draft-07/schema#",
713 "digest": {
714 "algorithm": DIGEST_ALGORITHM,
715 "scope": "exact UTF-8 bytes of docs/wire-schema.json"
716 },
717 "coverage": {
718 "complete": complete,
719 "covered": schemas.keys().collect::<Vec<_>>(),
720 "rpc_results": {
721 "total": all_rpc_methods.len(),
722 "covered": covered_rpc_methods,
723 "uncovered": uncovered_rpc_methods
724 },
725 "journal_event_payloads": {
726 "total": all_event_kinds.len(),
727 "covered": covered_event_kinds,
728 "uncovered": uncovered_event_kinds
729 },
730 "limitations": [
731 "journal.event covers the exact envelope and closed EventKind enum; coverage.journal_event_payloads.uncovered names every kind whose Event.data remains an open JSON object",
732 "coverage.rpc_results.uncovered is derived from the daemon dispatch inventory and names every result that still emits inline or otherwise lacks a schema from its real Rust type",
733 "the release version is reported at serve time in the server.schema result and is deliberately absent from these digested bytes, so the digest tracks wire shape alone"
734 ],
735 "follow_up": FOLLOW_UP_BEAD
736 },
737 "schemas": schemas
738 })
739}
740
741const MANAGED_AGENT_LIST_ROW_REQUIRED: &[&str] = &[
744 "id",
745 "name",
746 "command",
747 "args",
748 "cwd",
749 "env",
750 "restart",
751 "max_restarts",
752 "backoff_secs",
753 "auto_start",
754 "capabilities",
755 "status",
756 "pid",
757 "last_exit_code",
758 "restart_count",
759 "started_at",
760 "attached",
761 "manifest_path",
762 "log_path",
763 "stderr_log_path",
764];
765const DECLARATIVE_AGENT_ROW_REQUIRED: &[&str] = &[
766 "id",
767 "name",
768 "kind",
769 "enabled",
770 "capabilities",
771 "description",
772 "tools",
773 "goal",
774 "scenarios",
775];
776const DECLARATIVE_GOAL_REQUIRED: &[&str] = &["check", "max_iterations"];
777const EVENT_REQUIRED: &[&str] = &["kind", "data", "timestamp"];
778const INFERENCE_RESULT_REQUIRED: &[&str] = &[
779 "text",
780 "tool_calls",
781 "trace_id",
782 "model_used",
783 "requested_model_id",
784 "resolved_model_id",
785 "row_digest",
786 "catalog_revision",
787 "latency_ms",
788 "time_to_first_token_ms",
789 "usage",
790 "stop_reason",
791];
792const TOKEN_USAGE_REQUIRED: &[&str] = &[
793 "prompt_tokens",
794 "completion_tokens",
795 "total_tokens",
796 "context_window",
797 "cache_read_input_tokens",
798 "cache_creation_input_tokens",
799];
800const TOOL_CALL_REQUIRED: &[&str] = &["name", "arguments"];
801const THINKING_BLOCK_REQUIRED: &[&str] = &["text"];
802const BOUNDING_BOX_REQUIRED: &[&str] = &["x1", "y1", "x2", "y2"];
803const FALLBACK_FROM_REQUIRED: &[&str] = &["candidate", "reason"];
804const TOOL_SCHEMA_REQUIRED: &[&str] =
805 &["name", "source", "description", "parameters", "idempotent"];
806const TOOL_POLL_RESULT_REQUIRED: &[&str] = &["handle", "tool", "action_id", "status", "chunks"];
807const ACTION_RESULT_REQUIRED: &[&str] = &["action_id", "status", "state_changes", "timestamp"];
808const QUANTIZATION_OBJECT_REQUIRED: &[&str] = &["scheme", "label"];
809
810pub fn rendered_document() -> Vec<u8> {
812 let mut bytes = serde_json::to_vec_pretty(&document()).expect("wire schema serializes");
813 bytes.push(b'\n');
814 bytes
815}
816
817pub fn sha256_hex(bytes: &[u8]) -> String {
818 format!("{:x}", Sha256::digest(bytes))
819}
820
821pub fn rendered_digest_file(schema_bytes: &[u8]) -> Vec<u8> {
822 format!("{} wire-schema.json\n", sha256_hex(schema_bytes)).into_bytes()
823}
824
825fn committed_digest() -> Result<&'static str, String> {
826 let mut fields = COMMITTED_DIGEST.split_whitespace();
827 let digest = fields
828 .next()
829 .ok_or("committed wire schema digest is empty")?;
830 let filename = fields
831 .next()
832 .ok_or("committed wire schema digest omits its filename")?;
833 if fields.next().is_some() || filename != "wire-schema.json" {
834 return Err("committed wire schema digest must be '<sha256> wire-schema.json'".into());
835 }
836 if digest.len() != 64
837 || !digest
838 .bytes()
839 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
840 {
841 return Err("committed wire schema digest is not lowercase SHA-256".into());
842 }
843 Ok(digest)
844}
845
846pub fn committed_payload() -> Result<Value, String> {
849 let digest = committed_digest()?;
850 let actual = sha256_hex(COMMITTED_SCHEMA.as_bytes());
851 if actual != digest {
852 return Err(format!(
853 "embedded wire schema digest mismatch: expected {digest}, got {actual}"
854 ));
855 }
856 let schema: Value = serde_json::from_str(COMMITTED_SCHEMA)
857 .map_err(|error| format!("embedded wire schema is invalid JSON: {error}"))?;
858 serde_json::to_value(ServerSchemaResult {
859 schema,
860 digest: digest.to_string(),
861 digest_algorithm: DigestAlgorithm::Sha256,
862 car_version: env!("CARGO_PKG_VERSION").to_string(),
863 })
864 .map_err(|error| format!("server.schema payload does not serialize: {error}"))
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870 use std::collections::HashMap;
871
872 #[test]
873 fn repeated_generation_is_byte_identical() {
874 assert_eq!(rendered_document(), rendered_document());
875 assert_eq!(
876 rendered_digest_file(&rendered_document()),
877 rendered_digest_file(&rendered_document())
878 );
879 }
880
881 #[test]
890 fn a_version_bump_alone_cannot_change_the_digest() {
891 let document = document();
892 assert!(
893 document.get("car_version").is_none(),
894 "the digested document must carry no release version"
895 );
896 let rendered = String::from_utf8(rendered_document()).expect("schema is UTF-8");
897 assert!(
898 !rendered.contains(env!("CARGO_PKG_VERSION")),
899 "the crate version leaked into the digested bytes, so every release bump \
900 would change the digest and fail freshness on the bump commit"
901 );
902 }
903
904 #[test]
906 fn server_schema_reports_the_running_release() {
907 let payload = committed_payload().expect("committed payload");
908 assert_eq!(payload["car_version"], env!("CARGO_PKG_VERSION"));
909 }
910
911 #[test]
912 fn committed_artifacts_match_generation_and_rpc_payload() {
913 let generated = rendered_document();
914 assert_eq!(COMMITTED_SCHEMA.as_bytes(), generated);
915 assert_eq!(
916 COMMITTED_DIGEST.as_bytes(),
917 rendered_digest_file(&generated)
918 );
919
920 let payload = committed_payload().expect("committed payload");
921 assert_eq!(
922 crate::handler::handle_server_schema().expect("server.schema RPC payload"),
923 payload
924 );
925 assert_eq!(payload["schema"], document());
926 assert_eq!(payload["digest"], sha256_hex(&generated));
927 assert_eq!(payload["digest_algorithm"], DIGEST_ALGORITHM);
928 }
929
930 fn managed_agent() -> car_registry::supervisor::ManagedAgent {
931 car_registry::supervisor::ManagedAgent {
932 spec: car_registry::supervisor::AgentSpec {
933 id: "trader".into(),
934 name: "Trader".into(),
935 command: "/usr/local/bin/node".into(),
936 args: vec!["index.js".into()],
937 cwd: None,
938 env: BTreeMap::new(),
939 restart: car_registry::supervisor::RestartPolicy::OnFailure,
940 max_restarts: 10,
941 backoff_secs: 5,
942 auto_start: false,
943 token: "secret".into(),
944 method_allowlist: None,
945 capabilities: Vec::new(),
946 },
947 status: car_registry::supervisor::AgentStatus::Stopped,
948 pid: None,
949 last_exit_code: None,
950 restart_count: 0,
951 started_at: None,
952 blocked_by_pid: None,
953 }
954 }
955
956 fn declarative_spec() -> car_registry::declarative::DeclarativeAgentSpec {
957 car_registry::declarative::DeclarativeAgentSpec {
958 id: "newsroom".into(),
959 name: "Newsroom".into(),
960 identity: "You watch the journal.".into(),
961 tools: vec!["fs.read".into()],
962 denied_tools: Vec::new(),
963 standing_goal: String::new(),
964 goal: None,
965 cadence: None,
966 scenarios: Vec::new(),
967 builder_draft: None,
968 previous: None,
969 enabled: true,
970 context: Default::default(),
971 }
972 }
973
974 fn model_schema() -> car_inference::schema::ModelSchema {
975 car_inference::schema::ModelSchema {
979 id: "qwen/qwen3-4b".into(),
980 name: "Qwen3 4B".into(),
981 provider: "qwen".into(),
982 family: "qwen3".into(),
983 version: String::new(),
984 capabilities: vec![car_inference::schema::ModelCapability::Generate],
985 context_length: 32_768,
986 max_output_tokens: None,
987 param_count: String::new(),
988 quantization: None,
989 performance: Default::default(),
990 cost: Default::default(),
991 source: car_inference::schema::ModelSource::Ollama {
992 model_tag: "qwen3:4b".into(),
993 host: "http://localhost:11434".into(),
994 },
995 tags: Vec::new(),
996 supported_params: Vec::new(),
997 public_benchmarks: Vec::new(),
998 trust_tier: Default::default(),
999 deprecated: false,
1000 available: false,
1001 weights_ready: false,
1002 }
1003 }
1004
1005 fn inference_result() -> car_inference::InferenceResult {
1006 car_inference::InferenceResult {
1007 text: "hello".into(),
1008 tool_calls: Vec::new(),
1009 bounding_boxes: Vec::new(),
1010 trace_id: "trace-1".into(),
1011 model_used: "qwen/qwen3-4b".into(),
1012 model_identity: Default::default(),
1013 latency_ms: 12,
1014 time_to_first_token_ms: None,
1015 usage: None,
1016 provider_output_items: Vec::new(),
1017 thinking: Vec::new(),
1018 stop_reason: None,
1019 auth_fallback_from: None,
1020 local_last_resort: false,
1021 fallback_from: Vec::new(),
1022 }
1023 }
1024
1025 fn validate(document: &Value, key: &str, emitted: &Value) {
1026 let schema = &document["schemas"][key];
1027 let validator =
1028 jsonschema::validator_for(schema).unwrap_or_else(|e| panic!("{key} compiles: {e}"));
1029 if let Err(error) = validator.validate(emitted) {
1030 panic!(
1031 "the value {key} actually emits does not satisfy its generated schema: {error}\n\
1032 emitted: {}",
1033 serde_json::to_string_pretty(emitted).unwrap()
1034 );
1035 }
1036 }
1037
1038 #[test]
1046 fn every_covered_schema_validates_its_real_emitted_value() {
1047 let document = document();
1048 let mut checked: Vec<&str> = Vec::new();
1049
1050 let mut check = |key: &'static str, emitted: Value| {
1051 validate(&document, key, &emitted);
1052 checked.push(key);
1053 };
1054
1055 check(
1056 "cli.car_inspect.result",
1057 serde_json::to_value(CarInspectResult::Managed(managed_list_row())).unwrap(),
1058 );
1059 check(
1060 "journal.event",
1061 serde_json::to_value(minimal_event()).unwrap(),
1062 );
1063 check(
1064 "rpc.capabilities.list.result",
1065 serde_json::to_value(capabilities_list_result()).unwrap(),
1066 );
1067 check(
1068 "rpc.infer.result",
1069 serde_json::to_value(inference_result()).unwrap(),
1070 );
1071 check(
1072 "rpc.models.catalog_snapshot.result",
1073 serde_json::to_value(
1074 car_inference::catalog_identity::CatalogSnapshot::new([model_schema()])
1075 .expect("catalog snapshot"),
1076 )
1077 .unwrap(),
1078 );
1079 check(
1080 "rpc.server.handshake.result",
1081 serde_json::to_value(handshake_result()).unwrap(),
1082 );
1083 check(
1084 "rpc.server.schema.result",
1085 committed_payload().expect("committed payload"),
1086 );
1087 check(
1088 "rpc.state.get.result",
1089 json!({"arbitrary": [true, null, 3]}),
1090 );
1091 check(
1092 "rpc.state.set.result",
1093 serde_json::to_value(StateSetResult::Ok).unwrap(),
1094 );
1095 check(
1096 "rpc.state.exists.result",
1097 serde_json::to_value(StateExistsResult(false)).unwrap(),
1098 );
1099 check(
1100 "rpc.state.keys.result",
1101 serde_json::to_value(StateKeysResult(vec!["ready".into()])).unwrap(),
1102 );
1103 check(
1104 "rpc.state.snapshot.result",
1105 serde_json::to_value(StateSnapshotResult(serde_json::Map::from_iter([(
1106 "ready".into(),
1107 Value::Bool(true),
1108 )])))
1109 .unwrap(),
1110 );
1111 check(
1112 "rpc.tools.register.result",
1113 serde_json::to_value(ToolsRegisterResult(1)).unwrap(),
1114 );
1115 check(
1116 "rpc.tools.list.result",
1117 serde_json::to_value(ToolsListResult {
1118 tools: vec![minimal_tool_schema()],
1119 count: 1,
1120 })
1121 .unwrap(),
1122 );
1123 check(
1124 "rpc.tools.unregister.result",
1125 serde_json::to_value(ToolsUnregisterResult {
1126 unregistered: "fs.read".into(),
1127 removed: 1,
1128 })
1129 .unwrap(),
1130 );
1131 check(
1132 "rpc.tools.poll.result",
1133 serde_json::to_value(Some(minimal_tool_poll_result())).unwrap(),
1134 );
1135 check(
1136 "rpc.tools.cancel.result",
1137 serde_json::to_value(ToolsCancelResult { cancelled: true }).unwrap(),
1138 );
1139 check(
1140 "rpc.tools.stream.subscribe.result",
1141 serde_json::to_value(ToolsStreamSubscribeResult { subscribed: true }).unwrap(),
1142 );
1143 check(
1144 "type.action_result",
1145 serde_json::to_value(minimal_action_result()).unwrap(),
1146 );
1147
1148 let covered: Vec<String> = document["coverage"]["covered"]
1151 .as_array()
1152 .expect("covered list")
1153 .iter()
1154 .map(|value| value.as_str().expect("covered key").to_string())
1155 .collect();
1156 let mut checked: Vec<String> = checked.into_iter().map(str::to_string).collect();
1157 checked.sort();
1158 assert_eq!(
1159 covered, checked,
1160 "every covered schema needs a real emitted value checked against it"
1161 );
1162 }
1163
1164 #[test]
1166 fn declarative_rows_validate_against_the_inspect_schema() {
1167 let document = document();
1168 let spec = declarative_spec();
1169 validate(
1170 &document,
1171 "cli.car_inspect.result",
1172 &crate::coder::rpc::declarative_row(&spec),
1173 );
1174
1175 let mut with_goal = spec;
1176 with_goal.goal = Some(car_registry::declarative::DeclarativeGoal {
1177 check: "cargo test".into(),
1178 max_iterations: 8,
1179 });
1180 validate(
1181 &document,
1182 "cli.car_inspect.result",
1183 &crate::coder::rpc::declarative_row(&with_goal),
1184 );
1185 }
1186
1187 #[test]
1194 fn catalog_snapshot_declares_no_conditionally_emitted_field() {
1195 let document = document();
1196 let snapshot = car_inference::catalog_identity::CatalogSnapshot::new([model_schema()])
1197 .expect("catalog snapshot");
1198 validate(
1199 &document,
1200 "rpc.models.catalog_snapshot.result",
1201 &serde_json::to_value(&snapshot).unwrap(),
1202 );
1203 }
1204
1205 #[test]
1206 fn coverage_inventory_accounts_for_every_daemon_result_and_event_kind() {
1207 let document = document();
1208 let coverage = &document["coverage"];
1209
1210 let strings = |value: &Value| -> BTreeSet<String> {
1211 value
1212 .as_array()
1213 .expect("coverage list")
1214 .iter()
1215 .map(|value| value.as_str().expect("coverage name").to_string())
1216 .collect()
1217 };
1218
1219 let all_rpc: BTreeSet<String> = rpc_method_inventory()
1220 .into_iter()
1221 .map(str::to_string)
1222 .collect();
1223 let covered_rpc = strings(&coverage["rpc_results"]["covered"]);
1224 let uncovered_rpc = strings(&coverage["rpc_results"]["uncovered"]);
1225 assert!(covered_rpc.is_disjoint(&uncovered_rpc));
1226 assert_eq!(
1227 all_rpc,
1228 covered_rpc.union(&uncovered_rpc).cloned().collect(),
1229 "the source-derived daemon method inventory must be partitioned exactly"
1230 );
1231 assert_eq!(coverage["rpc_results"]["total"], Value::from(all_rpc.len()));
1232 for method in &covered_rpc {
1233 assert!(
1234 document["schemas"]
1235 .get(format!("rpc.{method}.result"))
1236 .is_some(),
1237 "covered RPC method has no result schema: {method}"
1238 );
1239 }
1240
1241 let event_schema = &document["schemas"]["journal.event"];
1242 let all_events = event_kind_inventory(event_schema);
1243 let covered_events = strings(&coverage["journal_event_payloads"]["covered"]);
1244 let uncovered_events = strings(&coverage["journal_event_payloads"]["uncovered"]);
1245 assert!(covered_events.is_disjoint(&uncovered_events));
1246 assert_eq!(
1247 all_events,
1248 covered_events.union(&uncovered_events).cloned().collect(),
1249 "the source-derived EventKind inventory must be partitioned exactly"
1250 );
1251 assert_eq!(
1252 coverage["journal_event_payloads"]["total"],
1253 Value::from(all_events.len())
1254 );
1255
1256 assert_eq!(
1257 coverage["complete"],
1258 Value::Bool(uncovered_rpc.is_empty() && uncovered_events.is_empty()),
1259 "coverage.complete may be true only when both source inventories are exhausted"
1260 );
1261 }
1262
1263 #[test]
1264 fn priority_enums_are_closed_and_objects_reject_unknown_fields() {
1265 let document = document();
1266 let event_kind = &document["schemas"]["journal.event"]["definitions"]["EventKind"];
1267 let variants = event_kind["oneOf"]
1268 .as_array()
1269 .expect("documented EventKind variants");
1270 assert!(!variants.is_empty());
1271 assert!(variants.iter().all(|variant| variant["enum"].is_array()));
1272 assert_eq!(
1273 document["schemas"]["type.action_result"]["additionalProperties"],
1274 false
1275 );
1276 assert_eq!(
1277 document["schemas"]["rpc.tools.list.result"]["additionalProperties"],
1278 false
1279 );
1280
1281 let validator = jsonschema::validator_for(&document["schemas"]["type.action_result"])
1282 .expect("ActionResult schema compiles");
1283 let valid = json!({
1284 "action_id": "action-1",
1285 "status": "succeeded",
1286 "state_changes": {},
1287 "timestamp": "2026-09-15T00:00:00Z"
1288 });
1289 assert!(validator.is_valid(&valid));
1290 let mut unknown = valid;
1291 unknown["unexpected"] = Value::Bool(true);
1292 assert!(!validator.is_valid(&unknown));
1293 }
1294
1295 fn managed_list_row() -> ManagedAgentListRow {
1296 ManagedAgentListRow {
1297 agent: ManagedAgentWire::from_managed(&managed_agent()),
1298 attached: false,
1299 tools: None,
1300 manifest_path: "/tmp/agents/trader/manifest.toml".into(),
1301 log_path: "/tmp/logs/trader.stdout.log".into(),
1302 stderr_log_path: "/tmp/logs/trader.stderr.log".into(),
1303 session_id: None,
1304 }
1305 }
1306
1307 fn capabilities_list_result() -> CapabilitiesListResult {
1308 CapabilitiesListResult {
1309 caller_role: CapabilityRole::Operator,
1310 count: 1,
1311 methods: vec![CapabilityMethodRow {
1312 method: "capabilities.list".into(),
1313 role: CapabilityRole::Operator,
1314 }],
1315 }
1316 }
1317
1318 fn minimal_tool_schema() -> car_ir::ToolSchema {
1319 car_ir::ToolSchema {
1320 name: "fs.read".into(),
1321 source: car_ir::ToolSourceKind::Builtin,
1322 description: "Read a file".into(),
1323 parameters: json!({"type": "object"}),
1324 returns: None,
1325 idempotent: true,
1326 cache_ttl_secs: None,
1327 rate_limit: None,
1328 }
1329 }
1330
1331 fn minimal_tool_poll_result() -> car_engine::tool_handles::ToolPollResult {
1332 car_engine::tool_handles::ToolPollResult {
1333 handle: "tool-1".into(),
1334 tool: "fs.read".into(),
1335 action_id: "action-1".into(),
1336 status: car_ir::ToolStatus::Running,
1337 chunks: Vec::new(),
1338 dropped_chunks: 0,
1339 result: None,
1340 error: None,
1341 }
1342 }
1343
1344 fn minimal_event() -> car_eventlog::Event {
1345 car_eventlog::Event {
1346 kind: car_eventlog::EventKind::ActionSucceeded,
1347 run_id: None,
1348 client_id: None,
1349 policy_session_id: None,
1350 action_id: None,
1351 proposal_id: None,
1352 data: HashMap::new(),
1353 timestamp: chrono::Utc::now(),
1354 prev_hash: None,
1355 hash: None,
1356 }
1357 }
1358
1359 fn minimal_action_result() -> car_ir::ActionResult {
1360 car_ir::ActionResult {
1361 action_id: "action-1".into(),
1362 status: car_ir::ActionStatus::Succeeded,
1363 output: None,
1364 error: None,
1365 terminal: false,
1366 rolled_back: false,
1367 state_changes: HashMap::new(),
1368 duration_ms: None,
1369 timestamp: chrono::Utc::now(),
1370 }
1371 }
1372
1373 fn handshake_result() -> ServerHandshakeResult {
1374 ServerHandshakeResult {
1375 protocol_version: car_proto::PROTOCOL_VERSION,
1376 server_version: env!("CARGO_PKG_VERSION").to_string(),
1377 client_protocol_version: u64::from(car_proto::PROTOCOL_VERSION),
1378 client_version: "unknown".into(),
1379 negotiated_capabilities: Vec::new(),
1380 assistant_name: "Parslee".into(),
1381 assistant_aliases: vec!["parslee".into()],
1382 assistant_brand: car_identity::BRAND_NAME.to_string(),
1383 }
1384 }
1385
1386 fn by_title<'a>(schema: &'a Value, title: &str) -> &'a Value {
1393 fn walk<'a>(
1394 value: &'a Value,
1395 title: &str,
1396 own_name: Option<&str>,
1397 entries_are_definitions: bool,
1398 ) -> Option<&'a Value> {
1399 match value {
1400 Value::Array(values) => values
1401 .iter()
1402 .find_map(|value| walk(value, title, None, false)),
1403 Value::Object(map) => {
1404 let named = own_name == Some(title)
1405 || map.get("title").and_then(Value::as_str) == Some(title);
1406 if named && map.contains_key("properties") {
1407 return Some(value);
1408 }
1409 map.iter().find_map(|(key, child)| {
1410 let child_name = entries_are_definitions.then_some(key.as_str());
1411 let child_defines = !entries_are_definitions && key == "definitions";
1412 walk(child, title, child_name, child_defines)
1413 })
1414 }
1415 _ => None,
1416 }
1417 }
1418 walk(schema, title, None, false)
1419 .unwrap_or_else(|| panic!("no generated object named {title}"))
1420 }
1421
1422 fn assert_required_matches_emitted(node: &Value, sample: &Value, label: &str) {
1431 let required: std::collections::BTreeSet<&str> = node["required"]
1432 .as_array()
1433 .unwrap_or_else(|| panic!("{label} declares no required list"))
1434 .iter()
1435 .map(|value| value.as_str().expect("required entries are strings"))
1436 .collect();
1437 let emitted: std::collections::BTreeSet<&str> = sample
1438 .as_object()
1439 .unwrap_or_else(|| panic!("{label} sample is not an object"))
1440 .keys()
1441 .map(String::as_str)
1442 .collect();
1443 assert_eq!(
1444 required, emitted,
1445 "{label}: `required` must name exactly the fields a minimal value emits"
1446 );
1447 }
1448
1449 #[test]
1450 fn required_lists_exactly_the_fields_a_minimal_value_emits() {
1451 let document = document();
1452 fn value<T: Serialize>(item: T) -> Value {
1453 serde_json::to_value(item).expect("wire value serializes")
1454 }
1455
1456 let inspect = &document["schemas"]["cli.car_inspect.result"];
1457 assert_required_matches_emitted(
1458 by_title(inspect, "ManagedAgentListRow"),
1459 &value(managed_list_row()),
1460 "ManagedAgentListRow",
1461 );
1462 assert_required_matches_emitted(
1463 by_title(inspect, "DeclarativeAgentRow"),
1464 &crate::coder::rpc::declarative_row(&declarative_spec()),
1465 "DeclarativeAgentRow",
1466 );
1467 assert_required_matches_emitted(
1468 by_title(inspect, "DeclarativeGoal"),
1469 &value(car_registry::declarative::DeclarativeGoal {
1470 check: "cargo test".into(),
1471 max_iterations: 8,
1472 }),
1473 "DeclarativeGoal",
1474 );
1475
1476 let event = &document["schemas"]["journal.event"];
1477 assert_required_matches_emitted(event, &value(minimal_event()), "Event");
1478
1479 let capabilities = &document["schemas"]["rpc.capabilities.list.result"];
1480 assert_required_matches_emitted(
1481 capabilities,
1482 &value(capabilities_list_result()),
1483 "CapabilitiesListResult",
1484 );
1485 assert_required_matches_emitted(
1486 by_title(capabilities, "CapabilityMethodRow"),
1487 &value(CapabilityMethodRow {
1488 method: "capabilities.list".into(),
1489 role: CapabilityRole::Operator,
1490 }),
1491 "CapabilityMethodRow",
1492 );
1493
1494 let inference = &document["schemas"]["rpc.infer.result"];
1495 assert_required_matches_emitted(inference, &value(inference_result()), "InferenceResult");
1496 assert_required_matches_emitted(
1497 by_title(inference, "TokenUsage"),
1498 &value(car_inference::TokenUsage::default()),
1499 "TokenUsage",
1500 );
1501 assert_required_matches_emitted(
1502 by_title(inference, "ToolCall"),
1503 &value(car_inference::tasks::generate::ToolCall {
1504 id: None,
1505 name: "fs.read".into(),
1506 arguments: HashMap::new(),
1507 }),
1508 "ToolCall",
1509 );
1510 assert_required_matches_emitted(
1511 by_title(inference, "ThinkingBlock"),
1512 &value(car_inference::tasks::generate::ThinkingBlock::default()),
1513 "ThinkingBlock",
1514 );
1515 assert_required_matches_emitted(
1516 by_title(inference, "BoundingBox"),
1517 &value(car_inference::tasks::grounding::BoundingBox {
1518 x1: 0,
1519 y1: 0,
1520 x2: 1,
1521 y2: 1,
1522 label: String::new(),
1523 confidence: None,
1524 }),
1525 "BoundingBox",
1526 );
1527 assert_required_matches_emitted(
1528 by_title(inference, "FallbackFrom"),
1529 &value(car_inference::FallbackFrom {
1530 candidate: "gpt-5".into(),
1531 reason: car_inference::FallbackReason::Failed,
1532 }),
1533 "FallbackFrom",
1534 );
1535
1536 let tools = &document["schemas"]["rpc.tools.list.result"];
1537 assert_required_matches_emitted(
1538 by_title(tools, "ToolSchema"),
1539 &value(minimal_tool_schema()),
1540 "ToolSchema",
1541 );
1542 let tool_poll = &document["schemas"]["rpc.tools.poll.result"];
1543 assert_required_matches_emitted(
1544 by_title(tool_poll, "ToolPollResult"),
1545 &value(minimal_tool_poll_result()),
1546 "ToolPollResult",
1547 );
1548
1549 let action_result = &document["schemas"]["type.action_result"];
1550 assert_required_matches_emitted(
1551 action_result,
1552 &value(minimal_action_result()),
1553 "ActionResult",
1554 );
1555
1556 let handshake = &document["schemas"]["rpc.server.handshake.result"];
1557 assert_required_matches_emitted(
1558 handshake,
1559 &value(handshake_result()),
1560 "ServerHandshakeResult",
1561 );
1562
1563 let schema_result = &document["schemas"]["rpc.server.schema.result"];
1564 assert_required_matches_emitted(
1565 schema_result,
1566 &committed_payload().expect("committed payload"),
1567 "ServerSchemaResult",
1568 );
1569 }
1570
1571 #[test]
1573 fn the_structured_quantization_form_omits_the_fields_its_label_carries() {
1574 let document = document();
1575 let catalog = &document["schemas"]["rpc.models.catalog_snapshot.result"];
1576 let quantization = car_inference::schema::Quantization {
1577 bits: None,
1578 scheme: car_inference::schema::QuantScheme::AffineGroupInt,
1579 group_size: None,
1580 label: "an-unparseable-label".into(),
1581 };
1582 let emitted = serde_json::to_value(&quantization).expect("quantization serializes");
1583 assert!(
1584 emitted.is_object(),
1585 "this label must take the structured form for the assertion to mean anything"
1586 );
1587 assert_required_matches_emitted(
1588 by_title(catalog, "QuantizationObjectWireSchema"),
1589 &emitted,
1590 "QuantizationObjectWireSchema",
1591 );
1592 }
1593
1594 #[test]
1596 fn the_managed_agent_projection_drops_the_agent_token() {
1597 let wire = serde_json::to_value(ManagedAgentWire::from_managed(&managed_agent())).unwrap();
1598 assert!(wire.get("token").is_none());
1599 assert!(!document()["schemas"]["cli.car_inspect.result"]
1600 .to_string()
1601 .contains("\"token\""));
1602 }
1603}