1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
5#[serde(transparent)]
6pub struct DiagnosticCode(pub String);
7
8impl DiagnosticCode {
9 pub fn new(value: impl Into<String>) -> Self {
10 Self(value.into())
11 }
12
13 pub fn as_str(&self) -> &str {
14 &self.0
15 }
16}
17
18impl From<&str> for DiagnosticCode {
19 fn from(value: &str) -> Self {
20 Self(value.to_string())
21 }
22}
23
24impl From<String> for DiagnosticCode {
25 fn from(value: String) -> Self {
26 Self(value)
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "camelCase")]
32pub struct Diagnostic {
33 pub severity: DiagnosticSeverity,
34 pub code: DiagnosticCode,
35 pub message: String,
36 pub source: Option<String>,
37 pub help: Option<String>,
38}
39
40impl Diagnostic {
41 pub fn new(
42 severity: DiagnosticSeverity,
43 code: impl Into<DiagnosticCode>,
44 message: impl Into<String>,
45 ) -> Self {
46 Self {
47 severity,
48 code: code.into(),
49 message: message.into(),
50 source: None,
51 help: None,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "camelCase")]
58pub enum DiagnosticSeverity {
59 Info,
60 Warning,
61 Error,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "camelCase")]
66pub struct RuntimeCapabilities {
67 pub native: bool,
68 pub server: bool,
69 pub wasm: bool,
70 pub mobile: MobileCapability,
71 pub requirements: Vec<RuntimeRequirement>,
72 pub max_recommended_input_bytes: Option<u64>,
73}
74
75impl RuntimeCapabilities {
76 pub fn pure_rust() -> Self {
77 Self {
78 native: true,
79 server: true,
80 wasm: true,
81 mobile: MobileCapability::Wasm,
82 requirements: Vec::new(),
83 max_recommended_input_bytes: None,
84 }
85 }
86
87 pub fn with_max_recommended_input_bytes(mut self, bytes: u64) -> Self {
88 self.max_recommended_input_bytes = Some(bytes);
89 self
90 }
91
92 pub fn with_requirement(
93 mut self,
94 name: impl Into<String>,
95 description: impl Into<String>,
96 required: bool,
97 ) -> Self {
98 self.requirements.push(RuntimeRequirement {
99 name: name.into(),
100 description: Some(description.into()),
101 required,
102 });
103 self
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "camelCase")]
109pub enum MobileCapability {
110 Native,
111 Wasm,
112 ApiOnly,
113 Unsupported,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "camelCase")]
118pub struct RuntimeRequirement {
119 pub name: String,
120 pub description: Option<String>,
121 pub required: bool,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[serde(transparent)]
126pub struct OperationId(pub String);
127
128impl OperationId {
129 pub fn new(value: impl Into<String>) -> Self {
130 Self(value.into())
131 }
132
133 pub fn as_str(&self) -> &str {
134 &self.0
135 }
136}
137
138impl From<&str> for OperationId {
139 fn from(value: &str) -> Self {
140 Self(value.to_string())
141 }
142}
143
144impl From<String> for OperationId {
145 fn from(value: String) -> Self {
146 Self(value)
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
151#[serde(rename_all = "camelCase")]
152pub struct OperationMetadata {
153 pub id: OperationId,
154 pub name: String,
155 pub description: Option<String>,
156 pub version: String,
157 pub capabilities: RuntimeCapabilities,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
161#[serde(rename_all = "camelCase")]
162pub struct PackageSurface {
163 pub library: String,
164 pub version: String,
165 pub operations: Vec<SurfaceOperation>,
166 pub capabilities: RuntimeCapabilities,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170#[serde(rename_all = "camelCase")]
171pub struct SurfaceOperation {
172 pub id: OperationId,
173 pub name: String,
174 pub description: Option<String>,
175 pub input_schema: serde_json::Value,
176 pub output_schema: serde_json::Value,
177 pub example_request: serde_json::Value,
178 pub wasm_supported: bool,
179 pub server_supported: bool,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183#[serde(rename_all = "camelCase")]
184pub enum SurfaceExecutionMode {
185 InMemory,
186 PlannedJob,
187 BackgroundJob,
188 ExternalCommand,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192#[serde(rename_all = "camelCase")]
193pub enum SurfaceSideEffect {
194 None,
195 ReadsFiles,
196 WritesFiles,
197 Network,
198 ExternalProcess,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
202#[serde(rename_all = "camelCase")]
203pub struct SurfaceArtifactExpectation {
204 pub id: String,
205 pub kind: String,
206 pub media_type: String,
207 pub required: bool,
208 pub description: Option<String>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(rename_all = "camelCase")]
213pub struct SurfaceExecutionPlan {
214 pub operation: OperationId,
215 pub mode: SurfaceExecutionMode,
216 pub side_effects: Vec<SurfaceSideEffect>,
217 pub cancellable: bool,
218 pub progress_unit: Option<String>,
219 pub expected_artifacts: Vec<SurfaceArtifactExpectation>,
220 pub requirements: Vec<RuntimeRequirement>,
221 pub max_recommended_input_bytes: Option<u64>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225#[serde(rename_all = "camelCase")]
226pub struct SurfaceRequest {
227 pub operation: OperationId,
228 pub input: serde_json::Value,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
232#[serde(rename_all = "camelCase")]
233pub struct SurfaceResponse {
234 pub operation: OperationId,
235 pub value: serde_json::Value,
236 pub diagnostics: Vec<Diagnostic>,
237 pub artifacts: Vec<serde_json::Value>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
241#[serde(rename_all = "camelCase")]
242pub struct SurfaceError {
243 pub code: String,
244 pub message: String,
245 pub operation: Option<OperationId>,
246 pub details: serde_json::Value,
247}
248
249impl SurfaceError {
250 pub fn invalid_request(
251 operation: Option<impl Into<OperationId>>,
252 message: impl Into<String>,
253 ) -> Self {
254 Self::new("invalid_request", operation, message, serde_json::json!({}))
255 }
256
257 pub fn unsupported_operation(
258 operation: impl Into<OperationId>,
259 package: impl Into<String>,
260 ) -> Self {
261 let operation = operation.into();
262 let package = package.into();
263 Self::new(
264 "unsupported_operation",
265 Some(operation.clone()),
266 format!(
267 "unsupported operation `{}` for {}",
268 operation.as_str(),
269 package
270 ),
271 serde_json::json!({"package": package}),
272 )
273 }
274
275 pub fn unsupported_value(
276 operation: Option<impl Into<OperationId>>,
277 field: impl Into<String>,
278 value: impl Into<String>,
279 allowed: &[&str],
280 ) -> Self {
281 let field = field.into();
282 let value = value.into();
283 Self::new(
284 "unsupported_value",
285 operation,
286 format!("unsupported value `{value}` for `{field}`"),
287 serde_json::json!({
288 "field": field,
289 "value": value,
290 "allowed": allowed
291 }),
292 )
293 }
294
295 pub fn resource_limit(
296 operation: Option<impl Into<OperationId>>,
297 field: impl Into<String>,
298 limit: usize,
299 actual: usize,
300 ) -> Self {
301 let field = field.into();
302 Self::new(
303 "resource_limit",
304 operation,
305 format!("`{field}` exceeds the maximum supported size of {limit}"),
306 serde_json::json!({
307 "field": field,
308 "limit": limit,
309 "actual": actual
310 }),
311 )
312 }
313
314 pub fn cancelled(operation: impl Into<OperationId>, message: impl Into<String>) -> Self {
315 Self::new("cancelled", Some(operation), message, serde_json::json!({}))
316 }
317
318 pub fn execution_failed(
319 operation: impl Into<OperationId>,
320 message: impl Into<String>,
321 details: serde_json::Value,
322 ) -> Self {
323 Self::new("execution_failed", Some(operation), message, details)
324 }
325
326 pub fn artifact_error(
327 operation: impl Into<OperationId>,
328 message: impl Into<String>,
329 details: serde_json::Value,
330 ) -> Self {
331 Self::new("artifact_error", Some(operation), message, details)
332 }
333
334 pub fn missing_dependency(
335 operation: Option<impl Into<OperationId>>,
336 dependency: impl Into<String>,
337 setup: impl Into<String>,
338 ) -> Self {
339 let dependency = dependency.into();
340 let setup = setup.into();
341 Self::new(
342 "missing_dependency",
343 operation,
344 format!("missing required dependency `{dependency}`"),
345 serde_json::json!({
346 "dependency": dependency,
347 "setup": setup
348 }),
349 )
350 }
351
352 pub fn new(
353 code: impl Into<String>,
354 operation: Option<impl Into<OperationId>>,
355 message: impl Into<String>,
356 details: serde_json::Value,
357 ) -> Self {
358 Self {
359 code: code.into(),
360 message: message.into(),
361 operation: operation.map(Into::into),
362 details,
363 }
364 }
365
366 pub fn to_error_string(&self) -> String {
367 serde_json::to_string(self).unwrap_or_else(|_| self.message.clone())
368 }
369}
370
371impl fmt::Display for SurfaceError {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 f.write_str(&self.message)
374 }
375}
376
377impl std::error::Error for SurfaceError {}
378
379pub fn parse_surface_error(error: &str) -> Option<SurfaceError> {
380 serde_json::from_str(error).ok()
381}
382
383pub fn parse_surface_input<T: for<'de> Deserialize<'de>>(
384 operation: Option<&str>,
385 input: serde_json::Value,
386) -> Result<T, String> {
387 serde_json::from_value(input).map_err(|error| {
388 SurfaceError::invalid_request(
389 operation.map(OperationId::new),
390 format!("invalid request: {error}"),
391 )
392 .to_error_string()
393 })
394}
395
396pub fn require_non_empty<T>(operation: &str, field: &str, values: &[T]) -> Result<(), String> {
397 if values.is_empty() {
398 Err(SurfaceError::invalid_request(
399 Some(OperationId::new(operation)),
400 format!("invalid request: {field} must not be empty"),
401 )
402 .to_error_string())
403 } else {
404 Ok(())
405 }
406}
407
408pub fn validate_max_items(
409 operation: &str,
410 field: &str,
411 actual: usize,
412 limit: usize,
413) -> Result<(), String> {
414 if actual > limit {
415 Err(
416 SurfaceError::resource_limit(Some(OperationId::new(operation)), field, limit, actual)
417 .to_error_string(),
418 )
419 } else {
420 Ok(())
421 }
422}
423
424pub fn validate_matching_lengths(
425 operation: &str,
426 left_field: &str,
427 left_len: usize,
428 right_field: &str,
429 right_len: usize,
430) -> Result<(), String> {
431 if left_len != right_len {
432 Err(SurfaceError::invalid_request(
433 Some(OperationId::new(operation)),
434 format!(
435 "invalid request: `{left_field}` length {left_len} must match `{right_field}` length {right_len}"
436 ),
437 )
438 .to_error_string())
439 } else {
440 Ok(())
441 }
442}
443
444pub fn surface_operation(
447 id: impl Into<String>,
448 name: impl Into<String>,
449 description: impl Into<String>,
450 example_request: serde_json::Value,
451) -> SurfaceOperation {
452 let id = id.into();
453 SurfaceOperation {
454 id: OperationId::new(id.clone()),
455 name: name.into(),
456 description: Some(description.into()),
457 input_schema: surface_input_schema(&id, &example_request),
458 output_schema: surface_output_schema(&id),
459 example_request,
460 wasm_supported: true,
461 server_supported: true,
462 }
463}
464
465pub fn surface_operation_with_execution_plan(
466 id: impl Into<String>,
467 name: impl Into<String>,
468 description: impl Into<String>,
469 example_request: serde_json::Value,
470 execution_plan: SurfaceExecutionPlan,
471) -> SurfaceOperation {
472 let mut operation = surface_operation(id, name, description, example_request);
473 let execution_plan = surface_execution_plan_value(&execution_plan);
474 insert_schema_extension(
475 &mut operation.input_schema,
476 "xExecutionPlan",
477 execution_plan.clone(),
478 );
479 insert_schema_extension(
480 &mut operation.output_schema,
481 "xExecutionPlan",
482 execution_plan,
483 );
484 operation
485}
486
487pub fn surface_execution_plan_value(plan: &SurfaceExecutionPlan) -> serde_json::Value {
488 serde_json::to_value(plan).unwrap_or_else(|_| serde_json::json!({}))
489}
490
491fn insert_schema_extension(schema: &mut serde_json::Value, key: &str, value: serde_json::Value) {
492 if let serde_json::Value::Object(object) = schema {
493 object.insert(key.to_string(), value);
494 }
495}
496
497pub fn surface_input_schema(
498 operation: &str,
499 example_request: &serde_json::Value,
500) -> serde_json::Value {
501 let properties = match example_request {
502 serde_json::Value::Object(object) => object
503 .iter()
504 .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
505 .collect::<serde_json::Map<_, _>>(),
506 _ => serde_json::Map::new(),
507 };
508 let required = required_fields_for_operation(operation, example_request);
509 serde_json::json!({
510 "type": "object",
511 "additionalProperties": false,
512 "properties": properties,
513 "required": required,
514 "xOperationCategory": operation_category(operation),
515 "xReleaseStability": "stable",
516 "xContractPolicy": "additiveOnly",
517 "xErrorShape": {
518 "code": "string",
519 "message": "string",
520 "operation": "string|null",
521 "details": "object"
522 },
523 "xResourceLimits": {
524 "maxRecommendedInputBytes": 1048576,
525 "largePayloadBehavior": "reject or deterministically truncate by operation-specific limit"
526 }
527 })
528}
529
530pub fn surface_output_schema(operation: &str) -> serde_json::Value {
531 serde_json::json!({
532 "type": "object",
533 "required": ["operation", "title", "message", "summary", "result"],
534 "properties": {
535 "operation": {"type": "string", "const": operation},
536 "title": {"type": "string", "minLength": 1},
537 "message": {"type": "string", "minLength": 1},
538 "summary": {"type": "object"},
539 "result": {}
540 },
541 "additionalProperties": true
542 })
543}
544
545pub fn operation_category(operation: &str) -> &'static str {
546 match operation {
547 "describe"
548 | "analysis.describe"
549 | "classification.models"
550 | "classification.schema"
551 | "embeddings.backends"
552 | "qa.models" => "debug",
553 "runtime.softmax" => "support",
554 _ => "workflow",
555 }
556}
557
558fn required_fields_for_operation(
559 operation: &str,
560 example_request: &serde_json::Value,
561) -> Vec<String> {
562 if operation == "describe" || operation.ends_with(".models") || operation.ends_with(".describe")
563 {
564 return Vec::new();
565 }
566 let optional = [
567 "dimensions",
568 "embedding",
569 "id",
570 "includeNearDuplicates",
571 "includePunctuation",
572 "includeSemanticNeighbors",
573 "keywordLimit",
574 "linguistics",
575 "lowercase",
576 "maxAlternatives",
577 "maxTokens",
578 "minTokensForDecision",
579 "mode",
580 "model",
581 "n",
582 "ngramSizes",
583 "normalizeWhitespace",
584 "options",
585 "order",
586 "profile",
587 "previewLimit",
588 "seed",
589 "sentenceLevel",
590 "shingleSizes",
591 "streamId",
592 "summarySentences",
593 "topK",
594 "truncation",
595 ];
596 match example_request {
597 serde_json::Value::Object(object) => object
598 .keys()
599 .filter(|key| !optional.contains(&key.as_str()))
600 .cloned()
601 .collect(),
602 _ => Vec::new(),
603 }
604}
605
606fn infer_schema_for_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
607 let mut schema = match value {
608 serde_json::Value::Bool(_) => serde_json::json!({"type": "boolean"}),
609 serde_json::Value::Number(number) if number.is_i64() || number.is_u64() => {
610 serde_json::json!({"type": "integer", "minimum": 0})
611 }
612 serde_json::Value::Number(_) => serde_json::json!({"type": "number"}),
613 serde_json::Value::String(_) => serde_json::json!({"type": "string", "minLength": 1}),
614 serde_json::Value::Array(values) => {
615 let item_schema = values
616 .first()
617 .map(|value| infer_schema_for_value("item", value))
618 .unwrap_or_else(|| serde_json::json!({}));
619 serde_json::json!({"type": "array", "items": item_schema, "minItems": 1})
620 }
621 serde_json::Value::Object(object) => serde_json::json!({
622 "type": "object",
623 "additionalProperties": true,
624 "properties": object
625 .iter()
626 .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
627 .collect::<serde_json::Map<_, _>>()
628 }),
629 serde_json::Value::Null => serde_json::json!({}),
630 };
631 if matches!(
632 key,
633 "topK" | "top_k" | "maxTokens" | "max_tokens" | "order" | "dimensions" | "n"
634 ) {
635 if let serde_json::Value::Object(object) = &mut schema {
636 object.insert("minimum".to_string(), serde_json::json!(1));
637 object.insert("maximum".to_string(), serde_json::json!(4096));
638 }
639 }
640 schema
641}
642
643pub fn describe_surface_response(
646 surface: &PackageSurface,
647 request: SurfaceRequest,
648) -> SurfaceResponse {
649 let result = serde_json::json!({
650 "library": &surface.library,
651 "version": &surface.version,
652 "operationCount": surface.operations.len(),
653 "operations": surface
654 .operations
655 .iter()
656 .map(|operation| operation.id.as_str())
657 .collect::<Vec<_>>(),
658 "input": request.input
659 });
660 structured_surface_response(
661 request.operation,
662 "Package surface metadata",
663 format!(
664 "{} exposes {} package-surface operations.",
665 surface.library,
666 surface.operations.len()
667 ),
668 serde_json::json!({
669 "operationCount": surface.operations.len(),
670 "runtime": {
671 "wasm": surface.capabilities.wasm,
672 "server": surface.capabilities.server,
673 "native": surface.capabilities.native
674 }
675 }),
676 result,
677 )
678}
679
680pub fn surface_response(operation: OperationId, value: serde_json::Value) -> SurfaceResponse {
682 let title = operation.as_str().to_string();
683 let message = format!("Ran package-surface operation `{}`.", operation.as_str());
684 let value = ensure_structured_surface_value(&operation, title, message, value);
685 SurfaceResponse {
686 operation,
687 value,
688 diagnostics: Vec::new(),
689 artifacts: Vec::new(),
690 }
691}
692
693pub fn structured_surface_value(
696 operation: &OperationId,
697 title: impl Into<String>,
698 message: impl Into<String>,
699 summary: serde_json::Value,
700 result: serde_json::Value,
701) -> serde_json::Value {
702 let mut object = match &result {
703 serde_json::Value::Object(map) => map.clone(),
704 _ => serde_json::Map::new(),
705 };
706 object.insert("title".to_string(), serde_json::Value::String(title.into()));
707 object.insert(
708 "operation".to_string(),
709 serde_json::Value::String(operation.as_str().to_string()),
710 );
711 object.insert(
712 "message".to_string(),
713 serde_json::Value::String(message.into()),
714 );
715 object.insert("summary".to_string(), summary);
716 object.insert("result".to_string(), result);
717 serde_json::Value::Object(object)
718}
719
720pub fn ensure_structured_surface_value(
723 operation: &OperationId,
724 title: impl Into<String>,
725 message: impl Into<String>,
726 value: serde_json::Value,
727) -> serde_json::Value {
728 let result = value.clone();
729 let mut object = match value {
730 serde_json::Value::Object(map) => map,
731 _ => serde_json::Map::new(),
732 };
733 object
734 .entry("operation".to_string())
735 .or_insert_with(|| serde_json::Value::String(operation.as_str().to_string()));
736 object
737 .entry("title".to_string())
738 .or_insert_with(|| serde_json::Value::String(title.into()));
739 object
740 .entry("message".to_string())
741 .or_insert_with(|| serde_json::Value::String(message.into()));
742 object
743 .entry("summary".to_string())
744 .or_insert_with(|| operation_summary(&result));
745 object.entry("result".to_string()).or_insert(result);
746 serde_json::Value::Object(object)
747}
748
749pub fn structured_surface_response(
751 operation: OperationId,
752 title: impl Into<String>,
753 message: impl Into<String>,
754 summary: serde_json::Value,
755 result: serde_json::Value,
756) -> SurfaceResponse {
757 let value = structured_surface_value(&operation, title, message, summary, result);
758 surface_response(operation, value)
759}
760
761pub fn structured_operation_response(
767 surface: &PackageSurface,
768 operation: OperationId,
769 result: serde_json::Value,
770) -> SurfaceResponse {
771 let metadata = surface
772 .operations
773 .iter()
774 .find(|candidate| candidate.id.as_str() == operation.as_str());
775 let title = metadata
776 .map(|operation| operation.name.clone())
777 .unwrap_or_else(|| operation.as_str().to_string());
778 let message = metadata
779 .and_then(|operation| operation.description.clone())
780 .unwrap_or_else(|| format!("Ran package-surface operation `{}`.", operation.as_str()));
781 let summary = operation_summary(&result);
782 structured_surface_response(operation, title, message, summary, result)
783}
784
785fn operation_summary(result: &serde_json::Value) -> serde_json::Value {
786 match result {
787 serde_json::Value::Object(object) => {
788 let mut summary = serde_json::Map::new();
789 summary.insert("status".to_string(), serde_json::json!("ok"));
790 for key in [
791 "count",
792 "width",
793 "height",
794 "format",
795 "pixelFormat",
796 "dimensions",
797 "operationCount",
798 ] {
799 if let Some(value) = object.get(key) {
800 summary.insert(key.to_string(), value.clone());
801 }
802 }
803 if let Some((key, value)) = object
804 .iter()
805 .find(|(_, value)| matches!(value, serde_json::Value::Array(_)))
806 {
807 summary.insert(
808 format!("{key}Count"),
809 serde_json::json!(value.as_array().map(Vec::len).unwrap_or(0)),
810 );
811 }
812 serde_json::Value::Object(summary)
813 }
814 serde_json::Value::Array(values) => {
815 serde_json::json!({"status": "ok", "count": values.len()})
816 }
817 _ => serde_json::json!({"status": "ok"}),
818 }
819}
820
821pub mod cli {
823 use std::fs;
824 use std::io::{self, Read};
825
826 use super::{
827 ensure_structured_surface_value, OperationId, PackageSurface, SurfaceRequest,
828 SurfaceResponse,
829 };
830
831 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
833 pub struct CliAdapterMetadata {
834 pub library_crate: &'static str,
835 pub surface_kind: &'static str,
836 pub library_import: &'static str,
837 pub server_package: &'static str,
838 pub app_package: &'static str,
839 pub wasm_package: &'static str,
840 }
841
842 pub fn package_metadata_json(metadata: CliAdapterMetadata, surface: PackageSurface) -> String {
844 serde_json::json!({
845 "package": format!("{}-cli", metadata.library_crate),
846 "surface": metadata.surface_kind,
847 "library": metadata.library_crate,
848 "libraryImport": metadata.library_import,
849 "serverPackage": metadata.server_package,
850 "appPackage": metadata.app_package,
851 "wasmPackage": metadata.wasm_package,
852 "operations": surface.operations
853 })
854 .to_string()
855 }
856
857 pub fn command_schema_json() -> String {
859 serde_json::json!({
860 "commands": [
861 {"name": "info", "description": "Print package and adapter metadata."},
862 {"name": "schema", "description": "Print the CLI command schema."},
863 {"name": "operations", "description": "Print library operations."},
864 {"name": "run", "description": "Run one library-owned operation."}
865 ]
866 })
867 .to_string()
868 }
869
870 pub fn read_json_input(
872 json: Option<String>,
873 file: Option<String>,
874 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
875 let input = if let Some(json) = json {
876 json
877 } else if let Some(file) = file {
878 fs::read_to_string(file)?
879 } else {
880 let mut buffer = String::new();
881 io::stdin().read_to_string(&mut buffer)?;
882 if buffer.trim().is_empty() {
883 "{}".to_string()
884 } else {
885 buffer
886 }
887 };
888 Ok(serde_json::from_str(&input)?)
889 }
890
891 pub fn run_wrapped_operation(
894 operation: &str,
895 input: serde_json::Value,
896 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
897 ) -> Result<SurfaceResponse, String> {
898 let mut response = runner(SurfaceRequest {
899 operation: OperationId::new(operation),
900 input,
901 })?;
902 let value = std::mem::take(&mut response.value);
903 response.value = ensure_structured_surface_value(
904 &response.operation,
905 operation.to_string(),
906 format!("Ran package-surface operation `{}`.", operation),
907 value,
908 );
909 Ok(response)
910 }
911}
912
913pub mod server {
915 use std::io::{self, BufRead, BufReader, Read, Write};
916 use std::net::{TcpListener, TcpStream};
917
918 use super::{
919 parse_surface_error, Diagnostic, DiagnosticSeverity, OperationId, PackageSurface,
920 SurfaceRequest, SurfaceResponse,
921 };
922
923 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
925 pub struct ServerAdapterMetadata {
926 pub library_crate: &'static str,
927 pub surface_kind: &'static str,
928 pub library_import: &'static str,
929 pub cli_package: &'static str,
930 pub app_package: &'static str,
931 pub wasm_package: &'static str,
932 }
933
934 #[derive(Debug, Clone, PartialEq, Eq)]
935 pub struct HttpResponse {
936 pub status_code: u16,
937 pub reason: &'static str,
938 pub content_type: &'static str,
939 pub body: String,
940 }
941
942 pub fn serve(
944 addr: &str,
945 metadata: ServerAdapterMetadata,
946 surface_provider: fn() -> PackageSurface,
947 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
948 ) -> io::Result<()> {
949 let listener = TcpListener::bind(addr)?;
950 for stream in listener.incoming() {
951 handle_stream(stream?, metadata, surface_provider, runner)?;
952 }
953 Ok(())
954 }
955
956 pub fn response_for(
958 method: &str,
959 path: &str,
960 body: &str,
961 metadata: ServerAdapterMetadata,
962 surface_provider: fn() -> PackageSurface,
963 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
964 ) -> HttpResponse {
965 match (method, path) {
966 ("OPTIONS", _) => HttpResponse {
967 status_code: 204,
968 reason: "No Content",
969 content_type: "application/json",
970 body: String::new(),
971 },
972 ("GET", "/health") => json_response(
973 200,
974 "OK",
975 serde_json::json!({
976 "ok": true,
977 "package": format!("{}-server", metadata.library_crate),
978 "library": metadata.library_crate
979 }),
980 ),
981 ("GET", "/api/package") => json_response(
982 200,
983 "OK",
984 package_metadata_value(metadata, surface_provider()),
985 ),
986 ("GET", "/api/schema") => {
987 json_response(200, "OK", schema_value(metadata, surface_provider()))
988 }
989 ("GET", "/api/operations") => {
990 json_response(200, "OK", serde_json::json!(surface_provider().operations))
991 }
992 ("POST", "/api/run") => run_response(body, metadata, runner),
993 ("POST", path) if path.starts_with("/api/") => {
994 let operation = path.trim_start_matches("/api/");
995 run_request(
996 SurfaceRequest {
997 operation: OperationId::new(operation),
998 input: parse_json_or_empty(body),
999 },
1000 metadata,
1001 runner,
1002 )
1003 }
1004 _ => json_response(
1005 404,
1006 "Not Found",
1007 serde_json::json!({
1008 "error": "not found",
1009 "path": path
1010 }),
1011 ),
1012 }
1013 }
1014
1015 pub fn package_metadata_json(
1017 metadata: ServerAdapterMetadata,
1018 surface: PackageSurface,
1019 ) -> String {
1020 package_metadata_value(metadata, surface).to_string()
1021 }
1022
1023 fn package_metadata_value(
1024 metadata: ServerAdapterMetadata,
1025 surface: PackageSurface,
1026 ) -> serde_json::Value {
1027 serde_json::json!({
1028 "package": format!("{}-server", metadata.library_crate),
1029 "surface": metadata.surface_kind,
1030 "library": metadata.library_crate,
1031 "libraryImport": metadata.library_import,
1032 "cliPackage": metadata.cli_package,
1033 "appPackage": metadata.app_package,
1034 "wasmPackage": metadata.wasm_package,
1035 "endpoints": [
1036 "GET /health",
1037 "GET /api/package",
1038 "GET /api/schema",
1039 "GET /api/operations",
1040 "POST /api/run",
1041 "POST /api/<operation-id>"
1042 ],
1043 "runtimeMetadata": {
1044 "candleDevice": serde_json::Value::Null
1045 },
1046 "operations": surface.operations
1047 })
1048 }
1049
1050 fn schema_value(metadata: ServerAdapterMetadata, surface: PackageSurface) -> serde_json::Value {
1051 let operations = surface
1052 .operations
1053 .into_iter()
1054 .map(|operation| {
1055 let path = format!("/api/{}", operation.id.as_str());
1056 (
1057 path,
1058 serde_json::json!({
1059 "post": {
1060 "summary": operation.name,
1061 "description": operation.description,
1062 "requestBody": operation.input_schema,
1063 "responses": {"200": operation.output_schema}
1064 }
1065 }),
1066 )
1067 })
1068 .collect::<serde_json::Map<_, _>>();
1069
1070 serde_json::json!({
1071 "openapi": "3.1.0",
1072 "info": {
1073 "title": format!("{} API", metadata.library_crate),
1074 "version": env!("CARGO_PKG_VERSION")
1075 },
1076 "paths": operations
1077 })
1078 }
1079
1080 fn run_response(
1081 body: &str,
1082 metadata: ServerAdapterMetadata,
1083 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1084 ) -> HttpResponse {
1085 let payload = match serde_json::from_str::<serde_json::Value>(body) {
1086 Ok(value) => value,
1087 Err(error) => {
1088 return diagnostic_response(
1089 400,
1090 "Bad Request",
1091 "invalid_request",
1092 &format!("invalid JSON: {error}"),
1093 metadata,
1094 );
1095 }
1096 };
1097 let operation = payload
1098 .get("operation")
1099 .and_then(serde_json::Value::as_str)
1100 .unwrap_or("describe")
1101 .to_string();
1102 let input = payload
1103 .get("input")
1104 .cloned()
1105 .unwrap_or_else(|| payload.clone());
1106 run_request(
1107 SurfaceRequest {
1108 operation: OperationId::new(operation),
1109 input,
1110 },
1111 metadata,
1112 runner,
1113 )
1114 }
1115
1116 fn run_request(
1117 request: SurfaceRequest,
1118 metadata: ServerAdapterMetadata,
1119 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1120 ) -> HttpResponse {
1121 match runner(request) {
1122 Ok(response) => json_response(200, "OK", serde_json::json!(response)),
1123 Err(error) => {
1124 diagnostic_response(400, "Bad Request", "operation_failed", &error, metadata)
1125 }
1126 }
1127 }
1128
1129 fn diagnostic_response(
1130 status_code: u16,
1131 reason: &'static str,
1132 code: &str,
1133 message: &str,
1134 metadata: ServerAdapterMetadata,
1135 ) -> HttpResponse {
1136 let parsed = parse_surface_error(message);
1137 let diagnostic_code = parsed
1138 .as_ref()
1139 .map(|error| error.code.as_str())
1140 .unwrap_or(code);
1141 let diagnostic_message = parsed
1142 .as_ref()
1143 .map(|error| error.message.as_str())
1144 .unwrap_or(message);
1145 let details = parsed
1146 .as_ref()
1147 .map(|error| error.details.clone())
1148 .unwrap_or_else(|| serde_json::json!({}));
1149 json_response(
1150 status_code,
1151 reason,
1152 serde_json::json!({
1153 "diagnostics": [Diagnostic {
1154 severity: DiagnosticSeverity::Error,
1155 code: diagnostic_code.into(),
1156 message: diagnostic_message.to_string(),
1157 source: Some(format!("{}-server", metadata.library_crate)),
1158 help: None,
1159 }],
1160 "error": {
1161 "code": diagnostic_code,
1162 "message": diagnostic_message,
1163 "details": details
1164 }
1165 }),
1166 )
1167 }
1168
1169 fn parse_json_or_empty(body: &str) -> serde_json::Value {
1170 if body.trim().is_empty() {
1171 serde_json::json!({})
1172 } else {
1173 serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({"raw": body}))
1174 }
1175 }
1176
1177 fn handle_stream(
1178 mut stream: TcpStream,
1179 metadata: ServerAdapterMetadata,
1180 surface_provider: fn() -> PackageSurface,
1181 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1182 ) -> io::Result<()> {
1183 let mut reader = BufReader::new(stream.try_clone()?);
1184 let mut request_line = String::new();
1185 reader.read_line(&mut request_line)?;
1186
1187 let mut content_length = 0usize;
1188 loop {
1189 let mut header = String::new();
1190 reader.read_line(&mut header)?;
1191 let trimmed = header.trim_end();
1192 if trimmed.is_empty() {
1193 break;
1194 }
1195 if let Some((name, value)) = trimmed.split_once(':') {
1196 if name.eq_ignore_ascii_case("content-length") {
1197 content_length = value.trim().parse().unwrap_or(0);
1198 }
1199 }
1200 }
1201
1202 let mut body = vec![0; content_length];
1203 if content_length > 0 {
1204 reader.read_exact(&mut body)?;
1205 }
1206 let body = String::from_utf8_lossy(&body);
1207
1208 let mut parts = request_line.split_whitespace();
1209 let method = parts.next().unwrap_or("GET");
1210 let path = parts.next().unwrap_or("/");
1211 let response = response_for(method, path, &body, metadata, surface_provider, runner);
1212 write_response(&mut stream, response)
1213 }
1214
1215 fn json_response(
1216 status_code: u16,
1217 reason: &'static str,
1218 value: serde_json::Value,
1219 ) -> HttpResponse {
1220 HttpResponse {
1221 status_code,
1222 reason,
1223 content_type: "application/json",
1224 body: value.to_string(),
1225 }
1226 }
1227
1228 fn write_response(stream: &mut TcpStream, response: HttpResponse) -> io::Result<()> {
1229 write!(
1230 stream,
1231 "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{}",
1232 response.status_code,
1233 response.reason,
1234 response.content_type,
1235 response.body.len(),
1236 response.body
1237 )
1238 }
1239}
1240
1241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1242#[serde(transparent)]
1243pub struct JobId(pub String);
1244
1245impl JobId {
1246 pub fn new(value: impl Into<String>) -> Self {
1247 Self(value.into())
1248 }
1249
1250 pub fn as_str(&self) -> &str {
1251 &self.0
1252 }
1253}
1254
1255impl From<&str> for JobId {
1256 fn from(value: &str) -> Self {
1257 Self(value.to_string())
1258 }
1259}
1260
1261impl From<String> for JobId {
1262 fn from(value: String) -> Self {
1263 Self(value)
1264 }
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1268#[serde(transparent)]
1269pub struct ArtifactId(pub String);
1270
1271impl ArtifactId {
1272 pub fn new(value: impl Into<String>) -> Self {
1273 Self(value.into())
1274 }
1275
1276 pub fn as_str(&self) -> &str {
1277 &self.0
1278 }
1279}
1280
1281impl From<&str> for ArtifactId {
1282 fn from(value: &str) -> Self {
1283 Self(value.to_string())
1284 }
1285}
1286
1287impl From<String> for ArtifactId {
1288 fn from(value: String) -> Self {
1289 Self(value)
1290 }
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295 use super::*;
1296
1297 #[test]
1298 fn diagnostic_uses_camel_case_json() {
1299 let diagnostic = Diagnostic::new(DiagnosticSeverity::Warning, "demo.warning", "check");
1300 let json = serde_json::to_string(&diagnostic).expect("serialize diagnostic");
1301
1302 assert!(json.contains("\"severity\":\"warning\""));
1303 assert!(json.contains("\"code\":\"demo.warning\""));
1304 }
1305
1306 #[test]
1307 fn pure_rust_capabilities_allow_wasm_and_server() {
1308 let capabilities = RuntimeCapabilities::pure_rust();
1309
1310 assert!(capabilities.native);
1311 assert!(capabilities.server);
1312 assert!(capabilities.wasm);
1313 assert_eq!(capabilities.mobile, MobileCapability::Wasm);
1314 }
1315
1316 #[test]
1317 fn capability_builders_preserve_pure_rust_defaults() {
1318 let capabilities = RuntimeCapabilities::pure_rust()
1319 .with_max_recommended_input_bytes(1024)
1320 .with_requirement("fixture", "test fixture input", false);
1321
1322 assert!(capabilities.native);
1323 assert!(capabilities.server);
1324 assert!(capabilities.wasm);
1325 assert_eq!(capabilities.max_recommended_input_bytes, Some(1024));
1326 assert_eq!(capabilities.requirements[0].name, "fixture");
1327 assert!(!capabilities.requirements[0].required);
1328 }
1329
1330 #[test]
1331 fn package_surface_uses_camel_case_json() {
1332 let surface = PackageSurface {
1333 library: "demo-core".to_string(),
1334 version: "0.1.0".to_string(),
1335 capabilities: RuntimeCapabilities::pure_rust(),
1336 operations: vec![SurfaceOperation {
1337 id: OperationId::new("describe"),
1338 name: "Describe".to_string(),
1339 description: Some("Describe package surface".to_string()),
1340 input_schema: serde_json::json!({"type": "object"}),
1341 output_schema: serde_json::json!({"type": "object"}),
1342 example_request: serde_json::json!({}),
1343 wasm_supported: true,
1344 server_supported: true,
1345 }],
1346 };
1347
1348 let json = serde_json::to_string(&surface).expect("serialize surface");
1349
1350 assert!(json.contains("\"inputSchema\""));
1351 assert!(json.contains("\"exampleRequest\""));
1352 assert!(json.contains("\"wasmSupported\":true"));
1353 }
1354
1355 #[test]
1356 fn surface_helpers_preserve_standard_response_shape() {
1357 let surface = PackageSurface {
1358 library: "demo".to_string(),
1359 version: "0.1.0".to_string(),
1360 capabilities: RuntimeCapabilities::pure_rust(),
1361 operations: vec![surface_operation(
1362 "describe",
1363 "Describe",
1364 "Describe demo package",
1365 serde_json::json!({"includeOperations": true}),
1366 )],
1367 };
1368 let response = describe_surface_response(
1369 &surface,
1370 SurfaceRequest {
1371 operation: OperationId::new("describe"),
1372 input: serde_json::json!({"includeOperations": true}),
1373 },
1374 );
1375
1376 assert_eq!(response.operation.as_str(), "describe");
1377 assert_eq!(response.value["library"], "demo");
1378 assert_eq!(response.value["operationCount"], 1);
1379 assert_eq!(response.diagnostics, Vec::new());
1380 assert_eq!(response.artifacts, Vec::<serde_json::Value>::new());
1381 }
1382
1383 #[test]
1384 fn surface_operation_declares_release_contract_schema() {
1385 let operation = surface_operation(
1386 "demo.run",
1387 "Run demo",
1388 "Run a demo workflow",
1389 serde_json::json!({"text": "hello", "topK": 3}),
1390 );
1391
1392 assert_eq!(operation.input_schema["additionalProperties"], false);
1393 assert_eq!(operation.input_schema["xOperationCategory"], "workflow");
1394 assert_eq!(operation.input_schema["xReleaseStability"], "stable");
1395 assert_eq!(
1396 operation.input_schema["required"],
1397 serde_json::json!(["text"])
1398 );
1399 assert_eq!(operation.input_schema["properties"]["topK"]["minimum"], 1);
1400 assert_eq!(operation.output_schema["required"][0], "operation");
1401 }
1402
1403 #[test]
1404 fn typed_surface_errors_roundtrip_for_transport_adapters() {
1405 let error = SurfaceError::unsupported_operation("demo.missing", "demo-package");
1406 let serialized = error.to_error_string();
1407 let parsed = parse_surface_error(&serialized).expect("typed surface error");
1408
1409 assert_eq!(parsed.code, "unsupported_operation");
1410 assert_eq!(parsed.operation.unwrap().as_str(), "demo.missing");
1411 assert!(parsed.message.contains("unsupported operation"));
1412 }
1413
1414 #[test]
1415 fn surface_error_is_standard_error_type() {
1416 let error = SurfaceError::invalid_request(Some("demo.run"), "invalid request: missing id");
1417 let boxed: Box<dyn std::error::Error> = Box::new(error.clone());
1418
1419 assert_eq!(boxed.to_string(), "invalid request: missing id");
1420 assert_eq!(error.to_string(), "invalid request: missing id");
1421 }
1422
1423 #[test]
1424 fn validation_helpers_return_typed_errors() {
1425 let limit = validate_max_items("demo.run", "values", 3, 2).expect_err("limit error");
1426 let parsed = parse_surface_error(&limit).expect("typed resource error");
1427 assert_eq!(parsed.code, "resource_limit");
1428 assert_eq!(parsed.details["field"], "values");
1429 assert_eq!(parsed.details["actual"], 3);
1430
1431 let length =
1432 validate_matching_lengths("demo.run", "left", 2, "right", 3).expect_err("length");
1433 let parsed = parse_surface_error(&length).expect("typed length error");
1434 assert_eq!(parsed.code, "invalid_request");
1435 assert!(parsed.message.contains("left"));
1436 assert!(parsed.message.contains("right"));
1437 }
1438
1439 #[test]
1440 fn execution_plan_serializes_to_schema_extension() {
1441 let plan = SurfaceExecutionPlan {
1442 operation: OperationId::new("demo.run"),
1443 mode: SurfaceExecutionMode::PlannedJob,
1444 side_effects: vec![SurfaceSideEffect::None],
1445 cancellable: true,
1446 progress_unit: Some("items".to_string()),
1447 expected_artifacts: vec![SurfaceArtifactExpectation {
1448 id: "report".to_string(),
1449 kind: "json".to_string(),
1450 media_type: "application/json".to_string(),
1451 required: true,
1452 description: Some("Structured report".to_string()),
1453 }],
1454 requirements: vec![RuntimeRequirement {
1455 name: "runtime-core".to_string(),
1456 description: Some("Pure Rust planner".to_string()),
1457 required: true,
1458 }],
1459 max_recommended_input_bytes: Some(1024),
1460 };
1461
1462 let operation = surface_operation_with_execution_plan(
1463 "demo.run",
1464 "Run demo",
1465 "Build a deterministic demo plan",
1466 serde_json::json!({"items": [1]}),
1467 plan,
1468 );
1469
1470 assert_eq!(
1471 operation.input_schema["xExecutionPlan"]["mode"],
1472 serde_json::json!("plannedJob")
1473 );
1474 assert_eq!(
1475 operation.output_schema["xExecutionPlan"]["expectedArtifacts"][0]["id"],
1476 serde_json::json!("report")
1477 );
1478 assert_eq!(
1479 operation.input_schema["xExecutionPlan"]["sideEffects"],
1480 serde_json::json!(["none"])
1481 );
1482 }
1483
1484 #[test]
1485 fn new_surface_error_constructors_are_typed_json() {
1486 let cancelled = SurfaceError::cancelled("demo.run", "cancelled by request");
1487 assert_eq!(cancelled.code, "cancelled");
1488 assert_eq!(cancelled.operation.unwrap().as_str(), "demo.run");
1489
1490 let execution = SurfaceError::execution_failed(
1491 "demo.run",
1492 "execution failed",
1493 serde_json::json!({
1494 "stage": "prepare"
1495 }),
1496 );
1497 assert_eq!(execution.code, "execution_failed");
1498 assert_eq!(execution.details["stage"], "prepare");
1499
1500 let artifact = SurfaceError::artifact_error(
1501 "demo.run",
1502 "artifact invalid",
1503 serde_json::json!({
1504 "artifact": "report"
1505 }),
1506 );
1507 assert_eq!(artifact.code, "artifact_error");
1508 assert_eq!(artifact.details["artifact"], "report");
1509 }
1510
1511 #[test]
1512 fn structured_operation_response_preserves_result_fields() {
1513 let surface = PackageSurface {
1514 library: "demo".to_string(),
1515 version: "0.1.0".to_string(),
1516 capabilities: RuntimeCapabilities::pure_rust(),
1517 operations: vec![surface_operation(
1518 "demo.run",
1519 "Run demo",
1520 "Run a demo workflow",
1521 serde_json::json!({"values": [1, 2]}),
1522 )],
1523 };
1524 let response = structured_operation_response(
1525 &surface,
1526 OperationId::new("demo.run"),
1527 serde_json::json!({"count": 2, "values": [1, 2]}),
1528 );
1529
1530 assert_eq!(response.value["count"], 2);
1531 assert_eq!(response.value["operation"], "demo.run");
1532 assert_eq!(response.value["title"], "Run demo");
1533 assert_eq!(response.value["summary"]["count"], 2);
1534 assert_eq!(
1535 response.value["result"]["values"],
1536 serde_json::json!([1, 2])
1537 );
1538 }
1539}