Skip to main content

ontocore_plugin/
protocol.rs

1use ontocore_core::{
2    validate_workspace_scope, Diagnostic, DiagnosticCode, DiagnosticSeverity, SourceLocation,
3};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6
7/// JSON wire format returned by subprocess plugins on stdout.
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct PluginOutput {
10    #[serde(default)]
11    pub diagnostics: Vec<PluginDiagnosticWire>,
12    #[serde(default)]
13    pub output_paths: Vec<String>,
14    #[serde(default)]
15    pub logs: Option<String>,
16    #[serde(default)]
17    pub exit_message: Option<String>,
18    /// Optional HTML payload for UI view contributions.
19    #[serde(default)]
20    pub view_html: Option<String>,
21    /// Non-fatal security violations (e.g. rejected paths) detected by the host.
22    #[serde(default)]
23    pub violations: Vec<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PluginDiagnosticWire {
28    pub code: String,
29    pub severity: String,
30    pub message: String,
31    pub file: String,
32    #[serde(default)]
33    pub line: Option<u64>,
34    #[serde(default)]
35    pub column: Option<u64>,
36    #[serde(default)]
37    pub entity_iri: Option<String>,
38}
39
40impl PluginDiagnosticWire {
41    pub fn into_diagnostic(self, plugin_id: &str, workspace: &Path) -> Diagnostic {
42        let requested = PathBuf::from(&self.file);
43        let (file, message) = match validate_workspace_scope(&requested, workspace) {
44            Ok(file) => (file, self.message),
45            Err(err) => (
46                workspace.to_path_buf(),
47                format!(
48                    "{} (rejected plugin diagnostic file path '{}': {})",
49                    self.message, self.file, err
50                ),
51            ),
52        };
53        Diagnostic {
54            code: DiagnosticCode::PluginViolation,
55            severity: match self.severity.as_str() {
56                "error" => DiagnosticSeverity::Error,
57                "warning" => DiagnosticSeverity::Warning,
58                _ => DiagnosticSeverity::Info,
59            },
60            message,
61            file,
62            range: SourceLocation {
63                line: self.line,
64                column: self.column,
65                start_byte: None,
66                end_byte: None,
67            },
68            entity_iri: self.entity_iri,
69            quick_fix: None,
70            plugin_id: Some(plugin_id.to_string()),
71            plugin_code: Some(self.code),
72        }
73    }
74}
75
76pub fn jail_output_paths(
77    plugin_id: &str,
78    workspace: &Path,
79    mut output: PluginOutput,
80) -> PluginOutput {
81    let mut jailed = Vec::new();
82    for p in output.output_paths.drain(..) {
83        let requested = PathBuf::from(&p);
84        match validate_workspace_scope(&requested, workspace) {
85            Ok(path) => jailed.push(path.display().to_string()),
86            Err(err) => output
87                .violations
88                .push(format!("rejected plugin output path '{p}' for {plugin_id}: {err}")),
89        }
90    }
91    output.output_paths = jailed;
92    output
93}
94
95pub fn plugin_diagnostic(
96    plugin_id: &str,
97    code: &str,
98    severity: DiagnosticSeverity,
99    message: impl Into<String>,
100    file: PathBuf,
101    entity_iri: Option<String>,
102) -> Diagnostic {
103    Diagnostic {
104        code: DiagnosticCode::PluginViolation,
105        severity,
106        message: message.into(),
107        file,
108        range: SourceLocation::default(),
109        entity_iri,
110        quick_fix: None,
111        plugin_id: Some(plugin_id.to_string()),
112        plugin_code: Some(code.to_string()),
113    }
114}