1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! Provider manifest and index data structures.
//!
//! These types model the JSON returned by provider-core `describe()` and the provider index
//! entries used by store, deployer, and runner components.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::{format, string::String, vec::Vec};
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{ErrorCode, GResult, GreenticError};
/// Canonical provider extension identifier stored in pack manifests.
pub const PROVIDER_EXTENSION_ID: &str = "greentic.provider-extension.v1";
/// Manifest describing a provider returned by `describe()`.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderManifest {
/// Provider type identifier (string-based to avoid enum coupling).
pub provider_type: String,
/// Capabilities advertised by the provider.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub capabilities: Vec<String>,
/// Operations exposed by the provider.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub ops: Vec<String>,
/// Optional JSON Schema reference for configuration.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub config_schema_ref: Option<String>,
/// Optional JSON Schema reference for provider state.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub state_schema_ref: Option<String>,
}
/// Runtime binding for a provider implementation.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderRuntimeRef {
/// Component identifier for the provider runtime.
pub component_ref: String,
/// Exported function implementing the provider runtime.
pub export: String,
/// WIT world for the provider runtime.
pub world: String,
}
/// Provider declaration stored in indexes and extension payloads.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderDecl {
/// Provider type identifier (string-based to avoid enum coupling).
pub provider_type: String,
/// Instance identifier for this provider declaration. Distinct from
/// `provider_type`: a single `provider_type` may be instanced multiple
/// times at the environment level, each instance carrying its own
/// `provider_id`. Absent when the declaration is single-instance.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub provider_id: Option<String>,
/// Capabilities advertised by the provider.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub capabilities: Vec<String>,
/// Operations exposed by the provider.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub ops: Vec<String>,
/// JSON Schema reference for configuration.
pub config_schema_ref: String,
/// Optional JSON Schema reference for provider state.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub state_schema_ref: Option<String>,
/// Runtime binding information for the provider.
pub runtime: ProviderRuntimeRef,
/// Optional documentation reference.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub docs_ref: Option<String>,
}
/// Inline extension payload embedding provider declarations.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderExtensionInline {
/// Providers included in the extension payload.
pub providers: Vec<ProviderDecl>,
/// Additional fields preserved for forward-compatibility.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "BTreeMap::is_empty", flatten)
)]
pub additional_fields: BTreeMap<String, Value>,
}
impl ProviderExtensionInline {
/// Performs basic structural validation without provider-specific semantics.
///
/// Rules:
/// - `provider_type` must be non-empty and globally unique within the inline.
/// - `provider_id`, when present, must be non-empty and globally unique
/// within the inline. Absent `provider_id` is permitted and marks the
/// declaration as single-instance.
/// - An explicit `provider_id` may equal its OWN `provider_type` (harmless
/// alias), but must NOT equal any OTHER declaration's `provider_type`.
/// This prevents a lookup keyed on `id == "teams"` from silently
/// resolving to a Slack runtime when a Slack decl declared
/// `provider_id: "teams"`.
/// - Runtime fields (`component_ref`, `export`, `world`) must be set.
pub fn validate_basic(&self) -> GResult<()> {
// Pass 1: per-provider structural checks + collect every provider_type.
// Building the complete provider_type set up front is what makes the
// cross-namespace check in Pass 2 order-independent (a provider_id on
// decl A can collide with a provider_type on a later-declared decl B).
let mut seen_types = BTreeSet::new();
for provider in &self.providers {
if provider.provider_type.is_empty() {
return Err(GreenticError::new(
ErrorCode::InvalidInput,
"ProviderDecl.provider_type must not be empty",
));
}
if !seen_types.insert(provider.provider_type.as_str()) {
return Err(GreenticError::new(
ErrorCode::InvalidInput,
format!(
"duplicate provider_type '{}' in ProviderExtensionInline",
provider.provider_type
),
));
}
if provider.runtime.component_ref.trim().is_empty()
|| provider.runtime.export.trim().is_empty()
|| provider.runtime.world.trim().is_empty()
{
return Err(GreenticError::new(
ErrorCode::InvalidInput,
format!(
"runtime fields must be set for provider '{}'",
provider.provider_type
),
));
}
}
// Pass 2: provider_id uniqueness + cross-namespace collision.
let mut seen_ids = BTreeSet::new();
for provider in &self.providers {
if let Some(id) = provider.provider_id.as_deref() {
if id.is_empty() {
return Err(GreenticError::new(
ErrorCode::InvalidInput,
format!(
"ProviderDecl.provider_id must not be empty when set (provider_type '{}')",
provider.provider_type
),
));
}
if !seen_ids.insert(id) {
return Err(GreenticError::new(
ErrorCode::InvalidInput,
format!("duplicate provider_id '{id}' in ProviderExtensionInline"),
));
}
if id != provider.provider_type && seen_types.contains(id) {
return Err(GreenticError::new(
ErrorCode::InvalidInput,
format!(
"provider_id '{id}' collides with another declaration's provider_type"
),
));
}
}
}
Ok(())
}
}