1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4pub mod landscape;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
7#[serde(transparent)]
8pub struct DiagnosticCode(pub String);
9
10impl DiagnosticCode {
11 pub fn new(value: impl Into<String>) -> Self {
12 Self(value.into())
13 }
14
15 pub fn as_str(&self) -> &str {
16 &self.0
17 }
18}
19
20impl From<&str> for DiagnosticCode {
21 fn from(value: &str) -> Self {
22 Self(value.to_string())
23 }
24}
25
26impl From<String> for DiagnosticCode {
27 fn from(value: String) -> Self {
28 Self(value)
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase")]
34pub struct Diagnostic {
35 pub severity: DiagnosticSeverity,
36 pub code: DiagnosticCode,
37 pub message: String,
38 pub source: Option<String>,
39 pub help: Option<String>,
40}
41
42impl Diagnostic {
43 pub fn new(
44 severity: DiagnosticSeverity,
45 code: impl Into<DiagnosticCode>,
46 message: impl Into<String>,
47 ) -> Self {
48 Self {
49 severity,
50 code: code.into(),
51 message: message.into(),
52 source: None,
53 help: None,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase")]
60pub enum DiagnosticSeverity {
61 Info,
62 Warning,
63 Error,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(rename_all = "camelCase")]
68pub struct RuntimeCapabilities {
69 pub native: bool,
70 pub server: bool,
71 pub wasm: bool,
72 pub mobile: MobileCapability,
73 pub requirements: Vec<RuntimeRequirement>,
74 pub max_recommended_input_bytes: Option<u64>,
75}
76
77impl RuntimeCapabilities {
78 pub fn pure_rust() -> Self {
79 Self {
80 native: true,
81 server: true,
82 wasm: true,
83 mobile: MobileCapability::Wasm,
84 requirements: Vec::new(),
85 max_recommended_input_bytes: None,
86 }
87 }
88
89 pub fn with_max_recommended_input_bytes(mut self, bytes: u64) -> Self {
90 self.max_recommended_input_bytes = Some(bytes);
91 self
92 }
93
94 pub fn with_requirement(
95 mut self,
96 name: impl Into<String>,
97 description: impl Into<String>,
98 required: bool,
99 ) -> Self {
100 self.requirements.push(RuntimeRequirement {
101 name: name.into(),
102 description: Some(description.into()),
103 required,
104 });
105 self
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110#[serde(rename_all = "camelCase")]
111pub enum MobileCapability {
112 Native,
113 Wasm,
114 ApiOnly,
115 Unsupported,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "camelCase")]
120pub struct RuntimeRequirement {
121 pub name: String,
122 pub description: Option<String>,
123 pub required: bool,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
127#[serde(transparent)]
128pub struct OperationId(pub String);
129
130impl OperationId {
131 pub fn new(value: impl Into<String>) -> Self {
132 Self(value.into())
133 }
134
135 pub fn as_str(&self) -> &str {
136 &self.0
137 }
138}
139
140impl From<&str> for OperationId {
141 fn from(value: &str) -> Self {
142 Self(value.to_string())
143 }
144}
145
146impl From<String> for OperationId {
147 fn from(value: String) -> Self {
148 Self(value)
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
153#[serde(rename_all = "camelCase")]
154pub struct OperationMetadata {
155 pub id: OperationId,
156 pub name: String,
157 pub description: Option<String>,
158 pub version: String,
159 pub capabilities: RuntimeCapabilities,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub struct PackageSurface {
165 pub library: String,
166 pub version: String,
167 pub operations: Vec<SurfaceOperation>,
168 pub capabilities: RuntimeCapabilities,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
172#[serde(rename_all = "camelCase")]
173pub struct SurfaceOperationCuration {
174 pub role: SurfaceOperationRole,
175 pub primary: bool,
176 pub sort_order: u16,
177}
178
179impl SurfaceOperationCuration {
180 pub fn workflow(sort_order: u16) -> Self {
181 Self {
182 role: SurfaceOperationRole::Workflow,
183 primary: false,
184 sort_order,
185 }
186 }
187
188 pub fn debug(sort_order: u16) -> Self {
189 Self {
190 role: SurfaceOperationRole::Debug,
191 primary: false,
192 sort_order,
193 }
194 }
195
196 pub fn support(sort_order: u16) -> Self {
197 Self {
198 role: SurfaceOperationRole::Support,
199 primary: false,
200 sort_order,
201 }
202 }
203
204 pub fn primary(mut self) -> Self {
205 self.primary = true;
206 self
207 }
208
209 pub fn from_operation_id(operation_id: &str) -> Self {
210 if operation_id == "describe" {
211 Self::debug(900)
212 } else {
213 match operation_category(operation_id) {
214 "debug" => Self::debug(900),
215 "support" => Self::support(500),
216 _ => Self::workflow(100),
217 }
218 }
219 }
220
221 fn legacy_category(&self) -> &'static str {
222 self.role.legacy_category()
223 }
224}
225
226impl Default for SurfaceOperationCuration {
227 fn default() -> Self {
228 Self::workflow(100)
229 }
230}
231
232#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
233#[serde(rename_all = "camelCase")]
234pub enum SurfaceOperationRole {
235 Workflow,
236 Debug,
237 Support,
238}
239
240impl SurfaceOperationRole {
241 fn legacy_category(self) -> &'static str {
242 match self {
243 Self::Workflow => "workflow",
244 Self::Debug => "debug",
245 Self::Support => "support",
246 }
247 }
248
249 fn from_legacy_category(value: &str) -> Option<Self> {
250 match value {
251 "workflow" => Some(Self::Workflow),
252 "debug" => Some(Self::Debug),
253 "support" => Some(Self::Support),
254 _ => None,
255 }
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260#[serde(rename_all = "camelCase")]
261pub struct SurfaceOperation {
262 pub id: OperationId,
263 pub name: String,
264 pub description: Option<String>,
265 #[serde(default)]
266 pub curation: SurfaceOperationCuration,
267 pub input_schema: serde_json::Value,
268 pub output_schema: serde_json::Value,
269 pub example_request: serde_json::Value,
270 pub wasm_supported: bool,
271 pub server_supported: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
275#[serde(rename_all = "camelCase")]
276pub enum SurfaceExecutionMode {
277 InMemory,
278 PlannedJob,
279 BackgroundJob,
280 ExternalCommand,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284#[serde(rename_all = "camelCase")]
285pub enum SurfaceSideEffect {
286 None,
287 ReadsFiles,
288 WritesFiles,
289 Network,
290 ExternalProcess,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
294#[serde(rename_all = "camelCase")]
295pub struct SurfaceArtifactExpectation {
296 pub id: String,
297 pub kind: String,
298 pub media_type: String,
299 pub required: bool,
300 pub description: Option<String>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "camelCase")]
305pub struct SurfaceExecutionPlan {
306 pub operation: OperationId,
307 pub mode: SurfaceExecutionMode,
308 pub side_effects: Vec<SurfaceSideEffect>,
309 pub cancellable: bool,
310 pub progress_unit: Option<String>,
311 pub expected_artifacts: Vec<SurfaceArtifactExpectation>,
312 pub requirements: Vec<RuntimeRequirement>,
313 pub max_recommended_input_bytes: Option<u64>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
317#[serde(rename_all = "camelCase")]
318pub struct SurfaceRuntimeContext {
319 pub runtime: SurfaceRuntimeKind,
320 pub side_effects: SurfaceSideEffectPolicy,
321 pub storage: SurfaceStorageContext,
322 pub model: SurfaceModelContext,
323}
324
325impl SurfaceRuntimeContext {
326 pub fn no_side_effects(runtime: SurfaceRuntimeKind) -> Self {
327 Self {
328 runtime,
329 side_effects: SurfaceSideEffectPolicy::none(),
330 storage: SurfaceStorageContext::default(),
331 model: SurfaceModelContext::plan_only(),
332 }
333 }
334
335 pub fn compatibility_no_side_effects() -> Self {
336 Self::no_side_effects(SurfaceRuntimeKind::Unknown)
337 }
338
339 pub fn allows_model_auto_setup(&self) -> bool {
340 self.model.auto_setup
341 && self.side_effects.allow_reads
342 && self.side_effects.allow_writes
343 && self.side_effects.allow_network
344 && self.storage.model_root.is_some()
345 && self.runtime.can_setup_models()
346 }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
350#[serde(rename_all = "camelCase")]
351pub enum SurfaceRuntimeKind {
352 NativeCli,
353 NativeServer,
354 Wasm,
355 Browser,
356 Mobile,
357 Unknown,
358}
359
360impl SurfaceRuntimeKind {
361 pub fn can_setup_models(&self) -> bool {
362 matches!(self, Self::NativeCli | Self::NativeServer)
363 }
364
365 pub fn can_run_external_processes(&self) -> bool {
366 matches!(self, Self::NativeCli | Self::NativeServer)
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
371#[serde(rename_all = "camelCase")]
372pub struct SurfaceSideEffectPolicy {
373 pub allow_reads: bool,
374 pub allow_writes: bool,
375 pub allow_network: bool,
376 pub allow_external_process: bool,
377 pub max_download_bytes: Option<u64>,
378}
379
380impl SurfaceSideEffectPolicy {
381 pub fn none() -> Self {
382 Self {
383 allow_reads: false,
384 allow_writes: false,
385 allow_network: false,
386 allow_external_process: false,
387 max_download_bytes: None,
388 }
389 }
390}
391
392#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
393#[serde(rename_all = "camelCase")]
394pub struct SurfaceStorageContext {
395 pub cache_root: Option<String>,
396 pub artifact_root: Option<String>,
397 pub model_root: Option<String>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
401#[serde(rename_all = "camelCase")]
402pub struct SurfaceModelContext {
403 pub auto_setup: bool,
404 pub preference: SurfaceModelExecutionPreference,
405}
406
407impl SurfaceModelContext {
408 pub fn plan_only() -> Self {
409 Self {
410 auto_setup: false,
411 preference: SurfaceModelExecutionPreference::PlanOnly,
412 }
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
417#[serde(rename_all = "camelCase")]
418pub enum SurfaceModelExecutionPreference {
419 ModelFirst,
420 PlanOnly,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
424#[serde(rename_all = "camelCase")]
425pub struct SurfaceRequest {
426 pub operation: OperationId,
427 pub input: serde_json::Value,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
431#[serde(rename_all = "camelCase")]
432pub struct SurfaceResponse {
433 pub operation: OperationId,
434 pub value: serde_json::Value,
435 pub diagnostics: Vec<Diagnostic>,
436 pub artifacts: Vec<serde_json::Value>,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
440#[serde(rename_all = "camelCase")]
441pub struct SurfaceError {
442 pub code: String,
443 pub message: String,
444 pub operation: Option<OperationId>,
445 pub details: serde_json::Value,
446}
447
448impl SurfaceError {
449 pub fn invalid_request(
450 operation: Option<impl Into<OperationId>>,
451 message: impl Into<String>,
452 ) -> Self {
453 Self::new("invalid_request", operation, message, serde_json::json!({}))
454 }
455
456 pub fn unsupported_operation(
457 operation: impl Into<OperationId>,
458 package: impl Into<String>,
459 ) -> Self {
460 let operation = operation.into();
461 let package = package.into();
462 Self::new(
463 "unsupported_operation",
464 Some(operation.clone()),
465 format!(
466 "unsupported operation `{}` for {}",
467 operation.as_str(),
468 package
469 ),
470 serde_json::json!({"package": package}),
471 )
472 }
473
474 pub fn unsupported_value(
475 operation: Option<impl Into<OperationId>>,
476 field: impl Into<String>,
477 value: impl Into<String>,
478 allowed: &[&str],
479 ) -> Self {
480 let field = field.into();
481 let value = value.into();
482 Self::new(
483 "unsupported_value",
484 operation,
485 format!("unsupported value `{value}` for `{field}`"),
486 serde_json::json!({
487 "field": field,
488 "value": value,
489 "allowed": allowed
490 }),
491 )
492 }
493
494 pub fn resource_limit(
495 operation: Option<impl Into<OperationId>>,
496 field: impl Into<String>,
497 limit: usize,
498 actual: usize,
499 ) -> Self {
500 let field = field.into();
501 Self::new(
502 "resource_limit",
503 operation,
504 format!("`{field}` exceeds the maximum supported size of {limit}"),
505 serde_json::json!({
506 "field": field,
507 "limit": limit,
508 "actual": actual
509 }),
510 )
511 }
512
513 pub fn cancelled(operation: impl Into<OperationId>, message: impl Into<String>) -> Self {
514 Self::new("cancelled", Some(operation), message, serde_json::json!({}))
515 }
516
517 pub fn execution_failed(
518 operation: impl Into<OperationId>,
519 message: impl Into<String>,
520 details: serde_json::Value,
521 ) -> Self {
522 Self::new("execution_failed", Some(operation), message, details)
523 }
524
525 pub fn artifact_error(
526 operation: impl Into<OperationId>,
527 message: impl Into<String>,
528 details: serde_json::Value,
529 ) -> Self {
530 Self::new("artifact_error", Some(operation), message, details)
531 }
532
533 pub fn missing_dependency(
534 operation: Option<impl Into<OperationId>>,
535 dependency: impl Into<String>,
536 setup: impl Into<String>,
537 ) -> Self {
538 let dependency = dependency.into();
539 let setup = setup.into();
540 Self::new(
541 "missing_dependency",
542 operation,
543 format!("missing required dependency `{dependency}`"),
544 serde_json::json!({
545 "dependency": dependency,
546 "setup": setup
547 }),
548 )
549 }
550
551 pub fn permission_denied(
552 operation: Option<impl Into<OperationId>>,
553 permission: impl Into<String>,
554 message: impl Into<String>,
555 ) -> Self {
556 let permission = permission.into();
557 Self::new(
558 "permission_denied",
559 operation,
560 message,
561 serde_json::json!({
562 "permission": permission
563 }),
564 )
565 }
566
567 pub fn new(
568 code: impl Into<String>,
569 operation: Option<impl Into<OperationId>>,
570 message: impl Into<String>,
571 details: serde_json::Value,
572 ) -> Self {
573 Self {
574 code: code.into(),
575 message: message.into(),
576 operation: operation.map(Into::into),
577 details,
578 }
579 }
580
581 pub fn to_error_string(&self) -> String {
582 serde_json::to_string(self).unwrap_or_else(|_| self.message.clone())
583 }
584}
585
586impl fmt::Display for SurfaceError {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 f.write_str(&self.message)
589 }
590}
591
592impl std::error::Error for SurfaceError {}
593
594pub fn parse_surface_error(error: &str) -> Option<SurfaceError> {
595 serde_json::from_str(error).ok()
596}
597
598pub fn parse_surface_input<T: for<'de> Deserialize<'de>>(
599 operation: Option<&str>,
600 input: serde_json::Value,
601) -> Result<T, String> {
602 serde_json::from_value(input).map_err(|error| {
603 SurfaceError::invalid_request(
604 operation.map(OperationId::new),
605 format!("invalid request: {error}"),
606 )
607 .to_error_string()
608 })
609}
610
611pub fn require_non_empty<T>(operation: &str, field: &str, values: &[T]) -> Result<(), String> {
612 if values.is_empty() {
613 Err(SurfaceError::invalid_request(
614 Some(OperationId::new(operation)),
615 format!("invalid request: {field} must not be empty"),
616 )
617 .to_error_string())
618 } else {
619 Ok(())
620 }
621}
622
623pub fn validate_max_items(
624 operation: &str,
625 field: &str,
626 actual: usize,
627 limit: usize,
628) -> Result<(), String> {
629 if actual > limit {
630 Err(
631 SurfaceError::resource_limit(Some(OperationId::new(operation)), field, limit, actual)
632 .to_error_string(),
633 )
634 } else {
635 Ok(())
636 }
637}
638
639pub fn validate_matching_lengths(
640 operation: &str,
641 left_field: &str,
642 left_len: usize,
643 right_field: &str,
644 right_len: usize,
645) -> Result<(), String> {
646 if left_len != right_len {
647 Err(SurfaceError::invalid_request(
648 Some(OperationId::new(operation)),
649 format!(
650 "invalid request: `{left_field}` length {left_len} must match `{right_field}` length {right_len}"
651 ),
652 )
653 .to_error_string())
654 } else {
655 Ok(())
656 }
657}
658
659pub fn surface_operation(
662 id: impl Into<String>,
663 name: impl Into<String>,
664 description: impl Into<String>,
665 example_request: serde_json::Value,
666) -> SurfaceOperation {
667 let id = id.into();
668 let curation = SurfaceOperationCuration::from_operation_id(&id);
669 let mut operation = SurfaceOperation {
670 id: OperationId::new(id.clone()),
671 name: name.into(),
672 description: Some(description.into()),
673 input_schema: surface_input_schema(&id, &example_request),
674 output_schema: surface_output_schema(&id),
675 curation: curation.clone(),
676 example_request,
677 wasm_supported: true,
678 server_supported: true,
679 };
680 set_surface_operation_curation(&mut operation, curation);
681 operation
682}
683
684pub fn surface_operation_with_execution_plan(
685 id: impl Into<String>,
686 name: impl Into<String>,
687 description: impl Into<String>,
688 example_request: serde_json::Value,
689 execution_plan: SurfaceExecutionPlan,
690) -> SurfaceOperation {
691 let mut operation = surface_operation(id, name, description, example_request);
692 let execution_plan = surface_execution_plan_value(&execution_plan);
693 insert_schema_extension(
694 &mut operation.input_schema,
695 "xExecutionPlan",
696 execution_plan.clone(),
697 );
698 insert_schema_extension(
699 &mut operation.output_schema,
700 "xExecutionPlan",
701 execution_plan,
702 );
703 operation
704}
705
706pub fn primary_workflow_operation(
707 id: impl Into<String>,
708 name: impl Into<String>,
709 description: impl Into<String>,
710 example_request: serde_json::Value,
711 execution_plan: Option<SurfaceExecutionPlan>,
712 lower_contracts: &[&str],
713) -> SurfaceOperation {
714 let mut operation = if let Some(plan) = execution_plan {
715 surface_operation_with_execution_plan(id, name, description, example_request, plan)
716 } else {
717 surface_operation(id, name, description, example_request)
718 };
719 set_surface_operation_curation(
720 &mut operation,
721 SurfaceOperationCuration::workflow(0).primary(),
722 );
723 if !lower_contracts.is_empty() {
724 let proof = serde_json::json!({
725 "policy": "primary workflow proves compatibility with lower crate contracts",
726 "crates": lower_contracts
727 });
728 insert_schema_extension(
729 &mut operation.input_schema,
730 "xLowerContractProof",
731 proof.clone(),
732 );
733 insert_schema_extension(&mut operation.output_schema, "xLowerContractProof", proof);
734 }
735 operation
736}
737
738pub fn add_surface_operation_schema_extension(
743 operation: &mut SurfaceOperation,
744 key: impl Into<String>,
745 value: serde_json::Value,
746) {
747 let key = key.into();
748 if key == "xOperationCategory" {
749 if let Some(category) = value
750 .as_str()
751 .and_then(SurfaceOperationRole::from_legacy_category)
752 {
753 operation.curation.role = category;
754 }
755 }
756 insert_schema_extension(&mut operation.input_schema, &key, value.clone());
757 insert_schema_extension(&mut operation.output_schema, &key, value);
758}
759
760pub fn set_surface_operation_curation(
761 operation: &mut SurfaceOperation,
762 curation: SurfaceOperationCuration,
763) {
764 let category = serde_json::json!(curation.legacy_category());
765 operation.curation = curation;
766 insert_schema_extension(
767 &mut operation.input_schema,
768 "xOperationCategory",
769 category.clone(),
770 );
771 insert_schema_extension(&mut operation.output_schema, "xOperationCategory", category);
772}
773
774pub fn landscape_operation_contract_value(
775 contract: &landscape::LandscapeOperationContract,
776) -> serde_json::Value {
777 serde_json::to_value(contract).unwrap_or_else(|_| serde_json::json!({}))
778}
779
780pub fn surface_operation_with_landscape(
781 id: impl Into<String>,
782 name: impl Into<String>,
783 description: impl Into<String>,
784 example_request: serde_json::Value,
785 contract: landscape::LandscapeOperationContract,
786) -> SurfaceOperation {
787 let mut operation = surface_operation(id, name, description, example_request);
788 attach_landscape_contract(&mut operation, contract);
789 operation
790}
791
792pub fn attach_landscape_contract(
793 operation: &mut SurfaceOperation,
794 contract: landscape::LandscapeOperationContract,
795) {
796 let landscape = landscape_operation_contract_value(&contract);
797 insert_schema_extension(&mut operation.input_schema, "xLandscape", landscape.clone());
798 insert_schema_extension(&mut operation.output_schema, "xLandscape", landscape);
799}
800
801pub fn surface_execution_plan_value(plan: &SurfaceExecutionPlan) -> serde_json::Value {
802 serde_json::to_value(plan).unwrap_or_else(|_| serde_json::json!({}))
803}
804
805fn insert_schema_extension(schema: &mut serde_json::Value, key: &str, value: serde_json::Value) {
806 if let serde_json::Value::Object(object) = schema {
807 object.insert(key.to_string(), value);
808 }
809}
810
811pub fn surface_input_schema(
812 operation: &str,
813 example_request: &serde_json::Value,
814) -> serde_json::Value {
815 let properties = match example_request {
816 serde_json::Value::Object(object) => object
817 .iter()
818 .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
819 .collect::<serde_json::Map<_, _>>(),
820 _ => serde_json::Map::new(),
821 };
822 let required = required_fields_for_operation(operation, example_request);
823 serde_json::json!({
824 "type": "object",
825 "additionalProperties": false,
826 "properties": properties,
827 "required": required,
828 "xOperationCategory": operation_category(operation),
829 "xReleaseStability": "stable",
830 "xContractPolicy": "additiveOnly",
831 "xErrorShape": {
832 "code": "string",
833 "message": "string",
834 "operation": "string|null",
835 "details": "object"
836 },
837 "xResourceLimits": {
838 "maxRecommendedInputBytes": 1048576,
839 "largePayloadBehavior": "reject or deterministically truncate by operation-specific limit"
840 }
841 })
842}
843
844pub fn surface_output_schema(operation: &str) -> serde_json::Value {
845 serde_json::json!({
846 "type": "object",
847 "required": ["operation", "title", "message", "summary", "result"],
848 "properties": {
849 "operation": {"type": "string", "const": operation},
850 "title": {"type": "string", "minLength": 1},
851 "message": {"type": "string", "minLength": 1},
852 "summary": {"type": "object"},
853 "result": {}
854 },
855 "additionalProperties": true
856 })
857}
858
859pub fn operation_category(operation: &str) -> &'static str {
860 match operation {
861 "describe"
862 | "analysis.describe"
863 | "classification.models"
864 | "classification.schema"
865 | "embeddings.backends"
866 | "index.open"
867 | "index.inspect"
868 | "qa.models" => "debug",
869 "index.removeDocuments" | "runtime.softmax" => "support",
870 _ => "workflow",
871 }
872}
873
874fn required_fields_for_operation(
875 operation: &str,
876 example_request: &serde_json::Value,
877) -> Vec<String> {
878 if operation == "describe" || operation.ends_with(".models") || operation.ends_with(".describe")
879 {
880 return Vec::new();
881 }
882 let optional = [
883 "dimensions",
884 "embedding",
885 "id",
886 "includeNearDuplicates",
887 "includePunctuation",
888 "includeSemanticNeighbors",
889 "keywordLimit",
890 "linguistics",
891 "lowercase",
892 "maxAlternatives",
893 "maxTokens",
894 "minTokensForDecision",
895 "mode",
896 "model",
897 "n",
898 "ngramSizes",
899 "normalizeWhitespace",
900 "options",
901 "order",
902 "profile",
903 "previewLimit",
904 "seed",
905 "sentenceLevel",
906 "shingleSizes",
907 "streamId",
908 "summarySentences",
909 "topK",
910 "truncation",
911 ];
912 match example_request {
913 serde_json::Value::Object(object) => object
914 .keys()
915 .filter(|key| !optional.contains(&key.as_str()))
916 .cloned()
917 .collect(),
918 _ => Vec::new(),
919 }
920}
921
922fn infer_schema_for_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
923 let mut schema = match value {
924 serde_json::Value::Bool(_) => serde_json::json!({"type": "boolean"}),
925 serde_json::Value::Number(number) if number.is_i64() || number.is_u64() => {
926 serde_json::json!({"type": "integer", "minimum": 0})
927 }
928 serde_json::Value::Number(_) => serde_json::json!({"type": "number"}),
929 serde_json::Value::String(_) => serde_json::json!({"type": "string", "minLength": 1}),
930 serde_json::Value::Array(values) => {
931 let item_schema = values
932 .first()
933 .map(|value| infer_schema_for_value("item", value))
934 .unwrap_or_else(|| serde_json::json!({}));
935 serde_json::json!({"type": "array", "items": item_schema, "minItems": 1})
936 }
937 serde_json::Value::Object(object) => serde_json::json!({
938 "type": "object",
939 "additionalProperties": true,
940 "properties": object
941 .iter()
942 .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
943 .collect::<serde_json::Map<_, _>>()
944 }),
945 serde_json::Value::Null => serde_json::json!({}),
946 };
947 if matches!(
948 key,
949 "topK" | "top_k" | "maxTokens" | "max_tokens" | "order" | "dimensions" | "n"
950 ) {
951 if let serde_json::Value::Object(object) = &mut schema {
952 object.insert("minimum".to_string(), serde_json::json!(1));
953 object.insert("maximum".to_string(), serde_json::json!(4096));
954 }
955 }
956 schema
957}
958
959pub fn describe_surface_response(
962 surface: &PackageSurface,
963 request: SurfaceRequest,
964) -> SurfaceResponse {
965 let result = serde_json::json!({
966 "library": &surface.library,
967 "version": &surface.version,
968 "operationCount": surface.operations.len(),
969 "operations": surface
970 .operations
971 .iter()
972 .map(|operation| operation.id.as_str())
973 .collect::<Vec<_>>(),
974 "input": request.input
975 });
976 structured_surface_response(
977 request.operation,
978 "Package surface metadata",
979 format!(
980 "{} exposes {} package-surface operations.",
981 surface.library,
982 surface.operations.len()
983 ),
984 serde_json::json!({
985 "operationCount": surface.operations.len(),
986 "runtime": {
987 "wasm": surface.capabilities.wasm,
988 "server": surface.capabilities.server,
989 "native": surface.capabilities.native
990 }
991 }),
992 result,
993 )
994}
995
996pub fn surface_response(operation: OperationId, value: serde_json::Value) -> SurfaceResponse {
998 let title = operation.as_str().to_string();
999 let message = format!("Ran package-surface operation `{}`.", operation.as_str());
1000 let value = ensure_structured_surface_value(&operation, title, message, value);
1001 SurfaceResponse {
1002 operation,
1003 value,
1004 diagnostics: Vec::new(),
1005 artifacts: Vec::new(),
1006 }
1007}
1008
1009pub fn structured_surface_value(
1012 operation: &OperationId,
1013 title: impl Into<String>,
1014 message: impl Into<String>,
1015 summary: serde_json::Value,
1016 result: serde_json::Value,
1017) -> serde_json::Value {
1018 let mut object = match &result {
1019 serde_json::Value::Object(map) => map.clone(),
1020 _ => serde_json::Map::new(),
1021 };
1022 object.insert("title".to_string(), serde_json::Value::String(title.into()));
1023 object.insert(
1024 "operation".to_string(),
1025 serde_json::Value::String(operation.as_str().to_string()),
1026 );
1027 object.insert(
1028 "message".to_string(),
1029 serde_json::Value::String(message.into()),
1030 );
1031 object.insert("summary".to_string(), summary);
1032 object.insert("result".to_string(), result);
1033 serde_json::Value::Object(object)
1034}
1035
1036pub fn ensure_structured_surface_value(
1039 operation: &OperationId,
1040 title: impl Into<String>,
1041 message: impl Into<String>,
1042 value: serde_json::Value,
1043) -> serde_json::Value {
1044 let result = value.clone();
1045 let mut object = match value {
1046 serde_json::Value::Object(map) => map,
1047 _ => serde_json::Map::new(),
1048 };
1049 object
1050 .entry("operation".to_string())
1051 .or_insert_with(|| serde_json::Value::String(operation.as_str().to_string()));
1052 object
1053 .entry("title".to_string())
1054 .or_insert_with(|| serde_json::Value::String(title.into()));
1055 object
1056 .entry("message".to_string())
1057 .or_insert_with(|| serde_json::Value::String(message.into()));
1058 object
1059 .entry("summary".to_string())
1060 .or_insert_with(|| operation_summary(&result));
1061 object.entry("result".to_string()).or_insert(result);
1062 serde_json::Value::Object(object)
1063}
1064
1065pub fn structured_surface_response(
1067 operation: OperationId,
1068 title: impl Into<String>,
1069 message: impl Into<String>,
1070 summary: serde_json::Value,
1071 result: serde_json::Value,
1072) -> SurfaceResponse {
1073 let value = structured_surface_value(&operation, title, message, summary, result);
1074 surface_response(operation, value)
1075}
1076
1077pub fn structured_operation_response(
1083 surface: &PackageSurface,
1084 operation: OperationId,
1085 result: serde_json::Value,
1086) -> SurfaceResponse {
1087 let metadata = surface
1088 .operations
1089 .iter()
1090 .find(|candidate| candidate.id.as_str() == operation.as_str());
1091 let title = metadata
1092 .map(|operation| operation.name.clone())
1093 .unwrap_or_else(|| operation.as_str().to_string());
1094 let message = metadata
1095 .and_then(|operation| operation.description.clone())
1096 .unwrap_or_else(|| format!("Ran package-surface operation `{}`.", operation.as_str()));
1097 let summary = operation_summary(&result);
1098 structured_surface_response(operation, title, message, summary, result)
1099}
1100
1101fn operation_summary(result: &serde_json::Value) -> serde_json::Value {
1102 match result {
1103 serde_json::Value::Object(object) => {
1104 let mut summary = serde_json::Map::new();
1105 summary.insert("status".to_string(), serde_json::json!("ok"));
1106 for key in [
1107 "count",
1108 "width",
1109 "height",
1110 "format",
1111 "pixelFormat",
1112 "dimensions",
1113 "operationCount",
1114 ] {
1115 if let Some(value) = object.get(key) {
1116 summary.insert(key.to_string(), value.clone());
1117 }
1118 }
1119 if let Some((key, value)) = object
1120 .iter()
1121 .find(|(_, value)| matches!(value, serde_json::Value::Array(_)))
1122 {
1123 summary.insert(
1124 format!("{key}Count"),
1125 serde_json::json!(value.as_array().map(Vec::len).unwrap_or(0)),
1126 );
1127 }
1128 serde_json::Value::Object(summary)
1129 }
1130 serde_json::Value::Array(values) => {
1131 serde_json::json!({"status": "ok", "count": values.len()})
1132 }
1133 _ => serde_json::json!({"status": "ok"}),
1134 }
1135}
1136
1137pub mod cli {
1139 use std::fs;
1140 use std::io::{self, Read};
1141
1142 use super::{
1143 ensure_structured_surface_value, OperationId, PackageSurface, SurfaceRequest,
1144 SurfaceResponse,
1145 };
1146
1147 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1149 pub struct CliAdapterMetadata {
1150 pub library_crate: &'static str,
1151 pub surface_kind: &'static str,
1152 pub library_import: &'static str,
1153 pub server_package: &'static str,
1154 pub app_package: &'static str,
1155 pub wasm_package: &'static str,
1156 }
1157
1158 pub fn package_metadata_json(metadata: CliAdapterMetadata, surface: PackageSurface) -> String {
1160 serde_json::json!({
1161 "package": format!("{}-cli", metadata.library_crate),
1162 "surface": metadata.surface_kind,
1163 "library": metadata.library_crate,
1164 "libraryImport": metadata.library_import,
1165 "serverPackage": metadata.server_package,
1166 "appPackage": metadata.app_package,
1167 "wasmPackage": metadata.wasm_package,
1168 "operations": surface.operations
1169 })
1170 .to_string()
1171 }
1172
1173 pub fn command_schema_json() -> String {
1175 serde_json::json!({
1176 "commands": [
1177 {"name": "info", "description": "Print package and adapter metadata."},
1178 {"name": "schema", "description": "Print the CLI command schema."},
1179 {"name": "operations", "description": "Print library operations."},
1180 {"name": "run", "description": "Run one library-owned operation."}
1181 ]
1182 })
1183 .to_string()
1184 }
1185
1186 pub fn read_json_input(
1188 json: Option<String>,
1189 file: Option<String>,
1190 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
1191 let input = if let Some(json) = json {
1192 json
1193 } else if let Some(file) = file {
1194 fs::read_to_string(file)?
1195 } else {
1196 let mut buffer = String::new();
1197 io::stdin().read_to_string(&mut buffer)?;
1198 if buffer.trim().is_empty() {
1199 "{}".to_string()
1200 } else {
1201 buffer
1202 }
1203 };
1204 Ok(serde_json::from_str(&input)?)
1205 }
1206
1207 pub fn run_wrapped_operation(
1210 operation: &str,
1211 input: serde_json::Value,
1212 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1213 ) -> Result<SurfaceResponse, String> {
1214 let mut response = runner(SurfaceRequest {
1215 operation: OperationId::new(operation),
1216 input,
1217 })?;
1218 let value = std::mem::take(&mut response.value);
1219 response.value = ensure_structured_surface_value(
1220 &response.operation,
1221 operation.to_string(),
1222 format!("Ran package-surface operation `{}`.", operation),
1223 value,
1224 );
1225 Ok(response)
1226 }
1227}
1228
1229pub mod server {
1231 use std::io::{self, BufRead, BufReader, Read, Write};
1232 use std::net::{TcpListener, TcpStream};
1233
1234 use super::{
1235 parse_surface_error, Diagnostic, DiagnosticSeverity, OperationId, PackageSurface,
1236 SurfaceRequest, SurfaceResponse,
1237 };
1238
1239 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1241 pub struct ServerAdapterMetadata {
1242 pub library_crate: &'static str,
1243 pub surface_kind: &'static str,
1244 pub library_import: &'static str,
1245 pub cli_package: &'static str,
1246 pub app_package: &'static str,
1247 pub wasm_package: &'static str,
1248 }
1249
1250 #[derive(Debug, Clone, PartialEq, Eq)]
1251 pub struct HttpResponse {
1252 pub status_code: u16,
1253 pub reason: &'static str,
1254 pub content_type: &'static str,
1255 pub body: String,
1256 }
1257
1258 pub fn serve(
1260 addr: &str,
1261 metadata: ServerAdapterMetadata,
1262 surface_provider: fn() -> PackageSurface,
1263 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1264 ) -> io::Result<()> {
1265 let listener = TcpListener::bind(addr)?;
1266 for stream in listener.incoming() {
1267 handle_stream(stream?, metadata, surface_provider, runner)?;
1268 }
1269 Ok(())
1270 }
1271
1272 pub fn response_for(
1274 method: &str,
1275 path: &str,
1276 body: &str,
1277 metadata: ServerAdapterMetadata,
1278 surface_provider: fn() -> PackageSurface,
1279 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1280 ) -> HttpResponse {
1281 match (method, path) {
1282 ("OPTIONS", _) => HttpResponse {
1283 status_code: 204,
1284 reason: "No Content",
1285 content_type: "application/json",
1286 body: String::new(),
1287 },
1288 ("GET", "/health") => json_response(
1289 200,
1290 "OK",
1291 serde_json::json!({
1292 "ok": true,
1293 "package": format!("{}-server", metadata.library_crate),
1294 "library": metadata.library_crate
1295 }),
1296 ),
1297 ("GET", "/api/package") => json_response(
1298 200,
1299 "OK",
1300 package_metadata_value(metadata, surface_provider()),
1301 ),
1302 ("GET", "/api/schema") => {
1303 json_response(200, "OK", schema_value(metadata, surface_provider()))
1304 }
1305 ("GET", "/api/operations") => {
1306 json_response(200, "OK", serde_json::json!(surface_provider().operations))
1307 }
1308 ("POST", "/api/run") => run_response(body, metadata, runner),
1309 ("POST", path) if path.starts_with("/api/") => {
1310 let operation = path.trim_start_matches("/api/");
1311 run_request(
1312 SurfaceRequest {
1313 operation: OperationId::new(operation),
1314 input: parse_json_or_empty(body),
1315 },
1316 metadata,
1317 runner,
1318 )
1319 }
1320 _ => json_response(
1321 404,
1322 "Not Found",
1323 serde_json::json!({
1324 "error": "not found",
1325 "path": path
1326 }),
1327 ),
1328 }
1329 }
1330
1331 pub fn package_metadata_json(
1333 metadata: ServerAdapterMetadata,
1334 surface: PackageSurface,
1335 ) -> String {
1336 package_metadata_value(metadata, surface).to_string()
1337 }
1338
1339 fn package_metadata_value(
1340 metadata: ServerAdapterMetadata,
1341 surface: PackageSurface,
1342 ) -> serde_json::Value {
1343 serde_json::json!({
1344 "package": format!("{}-server", metadata.library_crate),
1345 "surface": metadata.surface_kind,
1346 "library": metadata.library_crate,
1347 "libraryImport": metadata.library_import,
1348 "cliPackage": metadata.cli_package,
1349 "appPackage": metadata.app_package,
1350 "wasmPackage": metadata.wasm_package,
1351 "endpoints": [
1352 "GET /health",
1353 "GET /api/package",
1354 "GET /api/schema",
1355 "GET /api/operations",
1356 "POST /api/run",
1357 "POST /api/<operation-id>"
1358 ],
1359 "runtimeMetadata": {
1360 "candleDevice": serde_json::Value::Null
1361 },
1362 "operations": surface.operations
1363 })
1364 }
1365
1366 fn schema_value(metadata: ServerAdapterMetadata, surface: PackageSurface) -> serde_json::Value {
1367 let operations = surface
1368 .operations
1369 .into_iter()
1370 .map(|operation| {
1371 let path = format!("/api/{}", operation.id.as_str());
1372 (
1373 path,
1374 serde_json::json!({
1375 "post": {
1376 "summary": operation.name,
1377 "description": operation.description,
1378 "requestBody": operation.input_schema,
1379 "responses": {"200": operation.output_schema}
1380 }
1381 }),
1382 )
1383 })
1384 .collect::<serde_json::Map<_, _>>();
1385
1386 serde_json::json!({
1387 "openapi": "3.1.0",
1388 "info": {
1389 "title": format!("{} API", metadata.library_crate),
1390 "version": env!("CARGO_PKG_VERSION")
1391 },
1392 "paths": operations
1393 })
1394 }
1395
1396 fn run_response(
1397 body: &str,
1398 metadata: ServerAdapterMetadata,
1399 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1400 ) -> HttpResponse {
1401 let payload = match serde_json::from_str::<serde_json::Value>(body) {
1402 Ok(value) => value,
1403 Err(error) => {
1404 return diagnostic_response(
1405 400,
1406 "Bad Request",
1407 "invalid_request",
1408 &format!("invalid JSON: {error}"),
1409 metadata,
1410 );
1411 }
1412 };
1413 let operation = payload
1414 .get("operation")
1415 .and_then(serde_json::Value::as_str)
1416 .unwrap_or("describe")
1417 .to_string();
1418 let input = payload
1419 .get("input")
1420 .cloned()
1421 .unwrap_or_else(|| payload.clone());
1422 run_request(
1423 SurfaceRequest {
1424 operation: OperationId::new(operation),
1425 input,
1426 },
1427 metadata,
1428 runner,
1429 )
1430 }
1431
1432 fn run_request(
1433 request: SurfaceRequest,
1434 metadata: ServerAdapterMetadata,
1435 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1436 ) -> HttpResponse {
1437 match runner(request) {
1438 Ok(response) => json_response(200, "OK", serde_json::json!(response)),
1439 Err(error) => {
1440 diagnostic_response(400, "Bad Request", "operation_failed", &error, metadata)
1441 }
1442 }
1443 }
1444
1445 fn diagnostic_response(
1446 status_code: u16,
1447 reason: &'static str,
1448 code: &str,
1449 message: &str,
1450 metadata: ServerAdapterMetadata,
1451 ) -> HttpResponse {
1452 let parsed = parse_surface_error(message);
1453 let diagnostic_code = parsed
1454 .as_ref()
1455 .map(|error| error.code.as_str())
1456 .unwrap_or(code);
1457 let diagnostic_message = parsed
1458 .as_ref()
1459 .map(|error| error.message.as_str())
1460 .unwrap_or(message);
1461 let details = parsed
1462 .as_ref()
1463 .map(|error| error.details.clone())
1464 .unwrap_or_else(|| serde_json::json!({}));
1465 json_response(
1466 status_code,
1467 reason,
1468 serde_json::json!({
1469 "diagnostics": [Diagnostic {
1470 severity: DiagnosticSeverity::Error,
1471 code: diagnostic_code.into(),
1472 message: diagnostic_message.to_string(),
1473 source: Some(format!("{}-server", metadata.library_crate)),
1474 help: None,
1475 }],
1476 "error": {
1477 "code": diagnostic_code,
1478 "message": diagnostic_message,
1479 "details": details
1480 }
1481 }),
1482 )
1483 }
1484
1485 fn parse_json_or_empty(body: &str) -> serde_json::Value {
1486 if body.trim().is_empty() {
1487 serde_json::json!({})
1488 } else {
1489 serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({"raw": body}))
1490 }
1491 }
1492
1493 fn handle_stream(
1494 mut stream: TcpStream,
1495 metadata: ServerAdapterMetadata,
1496 surface_provider: fn() -> PackageSurface,
1497 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1498 ) -> io::Result<()> {
1499 let mut reader = BufReader::new(stream.try_clone()?);
1500 let mut request_line = String::new();
1501 reader.read_line(&mut request_line)?;
1502
1503 let mut content_length = 0usize;
1504 loop {
1505 let mut header = String::new();
1506 reader.read_line(&mut header)?;
1507 let trimmed = header.trim_end();
1508 if trimmed.is_empty() {
1509 break;
1510 }
1511 if let Some((name, value)) = trimmed.split_once(':') {
1512 if name.eq_ignore_ascii_case("content-length") {
1513 content_length = value.trim().parse().unwrap_or(0);
1514 }
1515 }
1516 }
1517
1518 let mut body = vec![0; content_length];
1519 if content_length > 0 {
1520 reader.read_exact(&mut body)?;
1521 }
1522 let body = String::from_utf8_lossy(&body);
1523
1524 let mut parts = request_line.split_whitespace();
1525 let method = parts.next().unwrap_or("GET");
1526 let path = parts.next().unwrap_or("/");
1527 let response = response_for(method, path, &body, metadata, surface_provider, runner);
1528 write_response(&mut stream, response)
1529 }
1530
1531 fn json_response(
1532 status_code: u16,
1533 reason: &'static str,
1534 value: serde_json::Value,
1535 ) -> HttpResponse {
1536 HttpResponse {
1537 status_code,
1538 reason,
1539 content_type: "application/json",
1540 body: value.to_string(),
1541 }
1542 }
1543
1544 fn write_response(stream: &mut TcpStream, response: HttpResponse) -> io::Result<()> {
1545 write!(
1546 stream,
1547 "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers: content-type\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nConnection: close\r\n\r\n{}",
1548 response.status_code,
1549 response.reason,
1550 response.content_type,
1551 response.body.len(),
1552 response.body
1553 )
1554 }
1555}
1556
1557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1558#[serde(transparent)]
1559pub struct JobId(pub String);
1560
1561impl JobId {
1562 pub fn new(value: impl Into<String>) -> Self {
1563 Self(value.into())
1564 }
1565
1566 pub fn as_str(&self) -> &str {
1567 &self.0
1568 }
1569}
1570
1571impl From<&str> for JobId {
1572 fn from(value: &str) -> Self {
1573 Self(value.to_string())
1574 }
1575}
1576
1577impl From<String> for JobId {
1578 fn from(value: String) -> Self {
1579 Self(value)
1580 }
1581}
1582
1583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1584#[serde(transparent)]
1585pub struct ArtifactId(pub String);
1586
1587impl ArtifactId {
1588 pub fn new(value: impl Into<String>) -> Self {
1589 Self(value.into())
1590 }
1591
1592 pub fn as_str(&self) -> &str {
1593 &self.0
1594 }
1595}
1596
1597impl From<&str> for ArtifactId {
1598 fn from(value: &str) -> Self {
1599 Self(value.to_string())
1600 }
1601}
1602
1603impl From<String> for ArtifactId {
1604 fn from(value: String) -> Self {
1605 Self(value)
1606 }
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611 use super::*;
1612
1613 #[test]
1614 fn diagnostic_uses_camel_case_json() {
1615 let diagnostic = Diagnostic::new(DiagnosticSeverity::Warning, "demo.warning", "check");
1616 let json = serde_json::to_string(&diagnostic).expect("serialize diagnostic");
1617
1618 assert!(json.contains("\"severity\":\"warning\""));
1619 assert!(json.contains("\"code\":\"demo.warning\""));
1620 }
1621
1622 #[test]
1623 fn pure_rust_capabilities_allow_wasm_and_server() {
1624 let capabilities = RuntimeCapabilities::pure_rust();
1625
1626 assert!(capabilities.native);
1627 assert!(capabilities.server);
1628 assert!(capabilities.wasm);
1629 assert_eq!(capabilities.mobile, MobileCapability::Wasm);
1630 }
1631
1632 #[test]
1633 fn capability_builders_preserve_pure_rust_defaults() {
1634 let capabilities = RuntimeCapabilities::pure_rust()
1635 .with_max_recommended_input_bytes(1024)
1636 .with_requirement("fixture", "test fixture input", false);
1637
1638 assert!(capabilities.native);
1639 assert!(capabilities.server);
1640 assert!(capabilities.wasm);
1641 assert_eq!(capabilities.max_recommended_input_bytes, Some(1024));
1642 assert_eq!(capabilities.requirements[0].name, "fixture");
1643 assert!(!capabilities.requirements[0].required);
1644 }
1645
1646 #[test]
1647 fn package_surface_uses_camel_case_json() {
1648 let surface = PackageSurface {
1649 library: "demo-core".to_string(),
1650 version: "0.1.0".to_string(),
1651 capabilities: RuntimeCapabilities::pure_rust(),
1652 operations: vec![SurfaceOperation {
1653 id: OperationId::new("describe"),
1654 name: "Describe".to_string(),
1655 description: Some("Describe package surface".to_string()),
1656 curation: SurfaceOperationCuration::from_operation_id("describe"),
1657 input_schema: serde_json::json!({"type": "object"}),
1658 output_schema: serde_json::json!({"type": "object"}),
1659 example_request: serde_json::json!({}),
1660 wasm_supported: true,
1661 server_supported: true,
1662 }],
1663 };
1664
1665 let json = serde_json::to_string(&surface).expect("serialize surface");
1666
1667 assert!(json.contains("\"inputSchema\""));
1668 assert!(json.contains("\"curation\""));
1669 assert!(json.contains("\"exampleRequest\""));
1670 assert!(json.contains("\"wasmSupported\":true"));
1671 }
1672
1673 #[test]
1674 fn surface_operation_curation_serializes_and_deserializes() {
1675 let operation = surface_operation(
1676 "demo.run",
1677 "Run demo",
1678 "Run a demo workflow",
1679 serde_json::json!({"text": "hello"}),
1680 );
1681
1682 let value = serde_json::to_value(&operation).expect("serialize operation");
1683 assert_eq!(value["curation"]["role"], "workflow");
1684 assert_eq!(value["curation"]["primary"], false);
1685 assert_eq!(value["curation"]["sortOrder"], 100);
1686
1687 let round_trip: SurfaceOperation =
1688 serde_json::from_value(value).expect("deserialize operation");
1689 assert_eq!(round_trip.curation, SurfaceOperationCuration::workflow(100));
1690 }
1691
1692 #[test]
1693 fn surface_operation_deserializes_without_curation() {
1694 let json = serde_json::json!({
1695 "id": "describe",
1696 "name": "Describe",
1697 "description": "Describe package surface",
1698 "inputSchema": {"type": "object", "xOperationCategory": "debug"},
1699 "outputSchema": {"type": "object", "xOperationCategory": "debug"},
1700 "exampleRequest": {},
1701 "wasmSupported": true,
1702 "serverSupported": true
1703 });
1704
1705 let operation: SurfaceOperation =
1706 serde_json::from_value(json).expect("deserialize old operation JSON");
1707
1708 assert_eq!(operation.id.as_str(), "describe");
1709 assert_eq!(operation.curation, SurfaceOperationCuration::default());
1710 }
1711
1712 #[test]
1713 fn set_surface_operation_curation_syncs_legacy_schema_category() {
1714 let mut operation = surface_operation(
1715 "demo.inspect",
1716 "Inspect demo",
1717 "Inspect demo inputs",
1718 serde_json::json!({}),
1719 );
1720
1721 set_surface_operation_curation(&mut operation, SurfaceOperationCuration::debug(750));
1722
1723 assert_eq!(operation.curation, SurfaceOperationCuration::debug(750));
1724 assert_eq!(operation.input_schema["xOperationCategory"], "debug");
1725 assert_eq!(operation.output_schema["xOperationCategory"], "debug");
1726 }
1727
1728 #[test]
1729 fn surface_helpers_preserve_standard_response_shape() {
1730 let surface = PackageSurface {
1731 library: "demo".to_string(),
1732 version: "0.1.0".to_string(),
1733 capabilities: RuntimeCapabilities::pure_rust(),
1734 operations: vec![surface_operation(
1735 "describe",
1736 "Describe",
1737 "Describe demo package",
1738 serde_json::json!({"includeOperations": true}),
1739 )],
1740 };
1741 let response = describe_surface_response(
1742 &surface,
1743 SurfaceRequest {
1744 operation: OperationId::new("describe"),
1745 input: serde_json::json!({"includeOperations": true}),
1746 },
1747 );
1748
1749 assert_eq!(response.operation.as_str(), "describe");
1750 assert_eq!(response.value["library"], "demo");
1751 assert_eq!(response.value["operationCount"], 1);
1752 assert_eq!(response.diagnostics, Vec::new());
1753 assert_eq!(response.artifacts, Vec::<serde_json::Value>::new());
1754 }
1755
1756 #[test]
1757 fn surface_operation_declares_release_contract_schema() {
1758 let operation = surface_operation(
1759 "demo.run",
1760 "Run demo",
1761 "Run a demo workflow",
1762 serde_json::json!({"text": "hello", "topK": 3}),
1763 );
1764
1765 assert_eq!(operation.input_schema["additionalProperties"], false);
1766 assert_eq!(operation.input_schema["xOperationCategory"], "workflow");
1767 assert_eq!(operation.input_schema["xReleaseStability"], "stable");
1768 assert_eq!(
1769 operation.input_schema["required"],
1770 serde_json::json!(["text"])
1771 );
1772 assert_eq!(operation.input_schema["properties"]["topK"]["minimum"], 1);
1773 assert_eq!(operation.output_schema["required"][0], "operation");
1774 }
1775
1776 #[test]
1777 fn surface_operation_schema_extension_updates_both_schemas() {
1778 let mut operation = surface_operation(
1779 "demo.compat",
1780 "Run compatibility demo",
1781 "Run a compatibility helper",
1782 serde_json::json!({"text": "hello"}),
1783 );
1784
1785 add_surface_operation_schema_extension(
1786 &mut operation,
1787 "xReplacementOperation",
1788 serde_json::json!("demo.run"),
1789 );
1790
1791 assert_eq!(operation.input_schema["xReplacementOperation"], "demo.run");
1792 assert_eq!(operation.output_schema["xReplacementOperation"], "demo.run");
1793 }
1794
1795 #[test]
1796 fn typed_surface_errors_roundtrip_for_transport_adapters() {
1797 let error = SurfaceError::unsupported_operation("demo.missing", "demo-package");
1798 let serialized = error.to_error_string();
1799 let parsed = parse_surface_error(&serialized).expect("typed surface error");
1800
1801 assert_eq!(parsed.code, "unsupported_operation");
1802 assert_eq!(parsed.operation.unwrap().as_str(), "demo.missing");
1803 assert!(parsed.message.contains("unsupported operation"));
1804 }
1805
1806 #[test]
1807 fn surface_error_is_standard_error_type() {
1808 let error = SurfaceError::invalid_request(Some("demo.run"), "invalid request: missing id");
1809 let boxed: Box<dyn std::error::Error> = Box::new(error.clone());
1810
1811 assert_eq!(boxed.to_string(), "invalid request: missing id");
1812 assert_eq!(error.to_string(), "invalid request: missing id");
1813 }
1814
1815 #[test]
1816 fn validation_helpers_return_typed_errors() {
1817 let limit = validate_max_items("demo.run", "values", 3, 2).expect_err("limit error");
1818 let parsed = parse_surface_error(&limit).expect("typed resource error");
1819 assert_eq!(parsed.code, "resource_limit");
1820 assert_eq!(parsed.details["field"], "values");
1821 assert_eq!(parsed.details["actual"], 3);
1822
1823 let length =
1824 validate_matching_lengths("demo.run", "left", 2, "right", 3).expect_err("length");
1825 let parsed = parse_surface_error(&length).expect("typed length error");
1826 assert_eq!(parsed.code, "invalid_request");
1827 assert!(parsed.message.contains("left"));
1828 assert!(parsed.message.contains("right"));
1829 }
1830
1831 #[test]
1832 fn execution_plan_serializes_to_schema_extension() {
1833 let plan = SurfaceExecutionPlan {
1834 operation: OperationId::new("demo.run"),
1835 mode: SurfaceExecutionMode::PlannedJob,
1836 side_effects: vec![SurfaceSideEffect::None],
1837 cancellable: true,
1838 progress_unit: Some("items".to_string()),
1839 expected_artifacts: vec![SurfaceArtifactExpectation {
1840 id: "report".to_string(),
1841 kind: "json".to_string(),
1842 media_type: "application/json".to_string(),
1843 required: true,
1844 description: Some("Structured report".to_string()),
1845 }],
1846 requirements: vec![RuntimeRequirement {
1847 name: "runtime-core".to_string(),
1848 description: Some("Pure Rust planner".to_string()),
1849 required: true,
1850 }],
1851 max_recommended_input_bytes: Some(1024),
1852 };
1853
1854 let operation = surface_operation_with_execution_plan(
1855 "demo.run",
1856 "Run demo",
1857 "Build a deterministic demo plan",
1858 serde_json::json!({"items": [1]}),
1859 plan,
1860 );
1861
1862 assert_eq!(
1863 operation.input_schema["xExecutionPlan"]["mode"],
1864 serde_json::json!("plannedJob")
1865 );
1866 assert_eq!(
1867 operation.output_schema["xExecutionPlan"]["expectedArtifacts"][0]["id"],
1868 serde_json::json!("report")
1869 );
1870 assert_eq!(
1871 operation.input_schema["xExecutionPlan"]["sideEffects"],
1872 serde_json::json!(["none"])
1873 );
1874 }
1875
1876 #[test]
1877 fn runtime_context_serializes_and_tracks_no_side_effect_defaults() {
1878 let context = SurfaceRuntimeContext::no_side_effects(SurfaceRuntimeKind::Wasm);
1879 assert!(!context.side_effects.allow_reads);
1880 assert!(!context.side_effects.allow_writes);
1881 assert!(!context.side_effects.allow_network);
1882 assert_eq!(
1883 context.model.preference,
1884 SurfaceModelExecutionPreference::PlanOnly
1885 );
1886 assert!(!context.allows_model_auto_setup());
1887
1888 let value = serde_json::to_value(&context).expect("serialize context");
1889 assert_eq!(value["runtime"], "wasm");
1890 assert_eq!(value["sideEffects"]["allowWrites"], false);
1891 assert_eq!(value["storage"]["modelRoot"], serde_json::Value::Null);
1892
1893 let round_trip: SurfaceRuntimeContext =
1894 serde_json::from_value(value).expect("deserialize context");
1895 assert_eq!(round_trip, context);
1896 }
1897
1898 #[test]
1899 fn primary_workflow_operation_attaches_strict_metadata_and_lower_contracts() {
1900 let operation = primary_workflow_operation(
1901 "demo.workflow",
1902 "Run workflow",
1903 "Runs the primary workflow.",
1904 serde_json::json!({"input": "value"}),
1905 None,
1906 &[
1907 "moritzbrantner-runtime-core",
1908 "moritzbrantner-video-analysis-core",
1909 ],
1910 );
1911
1912 assert_eq!(
1913 operation.input_schema["additionalProperties"],
1914 serde_json::json!(false)
1915 );
1916 assert_eq!(operation.input_schema["xOperationCategory"], "workflow");
1917 assert_eq!(operation.input_schema["xReleaseStability"], "stable");
1918 assert_eq!(operation.input_schema["xContractPolicy"], "additiveOnly");
1919 assert_eq!(
1920 operation.input_schema["xErrorShape"]["code"],
1921 serde_json::json!("string")
1922 );
1923 assert_eq!(
1924 operation.input_schema["xLowerContractProof"]["crates"][1],
1925 "moritzbrantner-video-analysis-core"
1926 );
1927 }
1928
1929 #[test]
1930 fn landscape_contract_serializes_to_schema_extension() {
1931 let contract = landscape::LandscapeOperationContract::new(
1932 landscape::LandscapeFunction::new("demo.curated", "moritzbrantner-runtime-core")
1933 .input(landscape::LandscapePort::new(
1934 "request",
1935 landscape::well_known::runtime_surface_request(),
1936 ))
1937 .output(landscape::LandscapePort::new(
1938 "response",
1939 landscape::well_known::runtime_surface_response(),
1940 )),
1941 );
1942
1943 let operation = surface_operation_with_landscape(
1944 "demo.run",
1945 "Run demo",
1946 "Run a curated demo workflow",
1947 serde_json::json!({"request": {"operation": "demo.run", "input": {}}}),
1948 contract,
1949 );
1950
1951 assert_eq!(operation.id.as_str(), "demo.run");
1952 assert_eq!(
1953 operation.example_request["request"]["operation"],
1954 "demo.run"
1955 );
1956 assert!(operation.wasm_supported);
1957 assert!(operation.server_supported);
1958 assert_eq!(
1959 operation.input_schema["xLandscape"]["function"]["id"],
1960 serde_json::json!("demo.curated")
1961 );
1962 assert_eq!(
1963 operation.input_schema["xLandscape"]["function"]["inputs"][0]["typeRef"]["id"],
1964 serde_json::json!("runtime.surfaceRequest")
1965 );
1966 assert_eq!(
1967 operation.output_schema["xLandscape"]["function"]["outputs"][0]["typeRef"]["rustType"],
1968 serde_json::json!("runtime_core::SurfaceResponse")
1969 );
1970 }
1971
1972 #[test]
1973 fn landscape_contract_value_uses_camel_case_json() {
1974 let contract = landscape::LandscapeOperationContract::new(
1975 landscape::LandscapeFunction::new("demo.curated", "moritzbrantner-runtime-core")
1976 .input(
1977 landscape::LandscapePort::new(
1978 "optionalRequest",
1979 landscape::well_known::runtime_surface_request(),
1980 )
1981 .optional(),
1982 )
1983 .output(
1984 landscape::LandscapePort::new(
1985 "responses",
1986 landscape::well_known::runtime_surface_response(),
1987 )
1988 .many(),
1989 ),
1990 );
1991 let value = landscape_operation_contract_value(&contract);
1992
1993 assert_eq!(value["function"]["stability"], "stable");
1994 assert_eq!(
1995 value["function"]["inputs"][0]["typeRef"]["schemaRef"],
1996 serde_json::Value::Null
1997 );
1998 assert_eq!(value["function"]["inputs"][0]["required"], false);
1999 assert_eq!(value["function"]["inputs"][0]["cardinality"], "optional");
2000 assert_eq!(value["function"]["outputs"][0]["cardinality"], "many");
2001 }
2002
2003 #[test]
2004 fn new_surface_error_constructors_are_typed_json() {
2005 let cancelled = SurfaceError::cancelled("demo.run", "cancelled by request");
2006 assert_eq!(cancelled.code, "cancelled");
2007 assert_eq!(cancelled.operation.unwrap().as_str(), "demo.run");
2008
2009 let execution = SurfaceError::execution_failed(
2010 "demo.run",
2011 "execution failed",
2012 serde_json::json!({
2013 "stage": "prepare"
2014 }),
2015 );
2016 assert_eq!(execution.code, "execution_failed");
2017 assert_eq!(execution.details["stage"], "prepare");
2018
2019 let artifact = SurfaceError::artifact_error(
2020 "demo.run",
2021 "artifact invalid",
2022 serde_json::json!({
2023 "artifact": "report"
2024 }),
2025 );
2026 assert_eq!(artifact.code, "artifact_error");
2027 assert_eq!(artifact.details["artifact"], "report");
2028 }
2029
2030 #[test]
2031 fn structured_operation_response_preserves_result_fields() {
2032 let surface = PackageSurface {
2033 library: "demo".to_string(),
2034 version: "0.1.0".to_string(),
2035 capabilities: RuntimeCapabilities::pure_rust(),
2036 operations: vec![surface_operation(
2037 "demo.run",
2038 "Run demo",
2039 "Run a demo workflow",
2040 serde_json::json!({"values": [1, 2]}),
2041 )],
2042 };
2043 let response = structured_operation_response(
2044 &surface,
2045 OperationId::new("demo.run"),
2046 serde_json::json!({"count": 2, "values": [1, 2]}),
2047 );
2048
2049 assert_eq!(response.value["count"], 2);
2050 assert_eq!(response.value["operation"], "demo.run");
2051 assert_eq!(response.value["title"], "Run demo");
2052 assert_eq!(response.value["summary"]["count"], 2);
2053 assert_eq!(
2054 response.value["result"]["values"],
2055 serde_json::json!([1, 2])
2056 );
2057 }
2058}