greentic_types/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4#![warn(clippy::unwrap_used, clippy::expect_used)]
5
6//! Shared types and helpers for Greentic multi-tenant flows.
7//!
8//! # Overview
9//! Greentic components share a single crate for tenancy, execution outcomes, network limits, and
10//! schema metadata. Use the strongly-typed identifiers to keep flows, packs, and components
11//! consistent across repositories and to benefit from serde + schema validation automatically.
12//!
13//! Component manifests now support optional development-time flows in `dev_flows`. These are JSON
14//! FlowIR structures used only during authoring (`greentic-dev`, `greentic-component`). Runtimes
15//! may safely ignore this field.
16//!
17//! ## Tenant contexts
18//! ```
19//! use greentic_types::{EnvId, TenantCtx, TenantId};
20//!
21//! let ctx = TenantCtx::new("prod".parse().unwrap(), "tenant-42".parse().unwrap())
22//!     .with_team(Some("team-ops".parse().unwrap()))
23//!     .with_user(Some("agent-007".parse().unwrap()));
24//! assert_eq!(ctx.tenant_id.as_str(), "tenant-42");
25//! ```
26//!
27//! ## Run results & serialization
28//! ```
29//! # #[cfg(feature = "time")] {
30//! use greentic_types::{FlowId, PackId, RunResult, RunStatus, SessionKey};
31//! use semver::Version;
32//! use time::OffsetDateTime;
33//!
34//! let now = OffsetDateTime::UNIX_EPOCH;
35//! let result = RunResult {
36//!     session_id: SessionKey::from("sess-1"),
37//!     pack_id: "greentic.demo.pack".parse().unwrap(),
38//!     pack_version: Version::parse("1.0.0").unwrap(),
39//!     flow_id: "demo-flow".parse().unwrap(),
40//!     started_at_utc: now,
41//!     finished_at_utc: now,
42//!     status: RunStatus::Success,
43//!     node_summaries: Vec::new(),
44//!     failures: Vec::new(),
45//!     artifacts_dir: None,
46//! };
47//! println!("{}", serde_json::to_string_pretty(&result).unwrap());
48//! # }
49//! ```
50//!
51//! Published JSON Schemas are listed in [`SCHEMAS.md`](SCHEMAS.md) and hosted under
52//! <https://greentic-ai.github.io/greentic-types/schemas/v1/>.
53
54extern crate alloc;
55
56/// Crate version string exposed for telemetry and capability negotiation.
57pub const VERSION: &str = env!("CARGO_PKG_VERSION");
58/// Base URL for all published JSON Schemas.
59pub const SCHEMA_BASE_URL: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1";
60
61pub mod bindings;
62pub mod capabilities;
63#[cfg(feature = "std")]
64pub mod cbor;
65pub mod component;
66pub mod component_source;
67pub mod deployment;
68pub mod distributor;
69pub mod events;
70pub mod events_provider;
71pub mod flow;
72pub mod flow_resolve;
73pub mod flow_resolve_summary;
74pub mod messaging;
75pub mod pack_manifest;
76pub mod provider;
77pub mod store;
78pub mod supply_chain;
79pub mod worker;
80
81pub mod context;
82pub mod error;
83pub mod outcome;
84pub mod pack;
85pub mod policy;
86pub mod run;
87#[cfg(all(feature = "schemars", feature = "std"))]
88pub mod schema;
89pub mod secrets;
90pub mod session;
91pub mod state;
92pub mod telemetry;
93pub mod tenant;
94pub mod tenant_config;
95
96pub use bindings::hints::{
97    BindingsHints, EnvHints, McpHints, McpServer, NetworkHints, SecretsHints,
98};
99pub use capabilities::{
100    Capabilities, FsCaps, HttpCaps, KvCaps, Limits, NetCaps, SecretsCaps, TelemetrySpec, ToolsCaps,
101};
102#[cfg(feature = "std")]
103pub use cbor::{CborError, decode_pack_manifest, encode_pack_manifest};
104pub use component::{
105    ComponentCapabilities, ComponentConfigurators, ComponentDevFlow, ComponentManifest,
106    ComponentOperation, ComponentProfileError, ComponentProfiles, EnvCapabilities,
107    EventsCapabilities, FilesystemCapabilities, FilesystemMode, FilesystemMount, HostCapabilities,
108    HttpCapabilities, IaCCapabilities, MessagingCapabilities, ResourceHints, SecretsCapabilities,
109    StateCapabilities, TelemetryCapabilities, TelemetryScope, WasiCapabilities,
110};
111pub use component_source::{ComponentSourceRef, ComponentSourceRefError};
112pub use context::{Cloud, DeploymentCtx, Platform};
113pub use deployment::{
114    ChannelPlan, DeploymentPlan, MessagingPlan, MessagingSubjectPlan, OAuthPlan, RunnerPlan,
115    TelemetryPlan,
116};
117pub use distributor::{
118    ArtifactLocation, CacheInfo, ComponentDigest, ComponentStatus, DistributorEnvironmentId,
119    PackStatusResponseV2, ResolveComponentRequest, ResolveComponentResponse, SignatureSummary,
120};
121pub use error::{ErrorCode, GResult, GreenticError};
122pub use events::{EventEnvelope, EventId, EventMetadata};
123pub use events_provider::{
124    EventProviderDescriptor, EventProviderKind, OrderingKind, ReliabilityKind, TransportKind,
125};
126pub use flow::{
127    ComponentRef as FlowComponentRef, Flow, FlowKind, FlowMetadata, InputMapping, Node,
128    OutputMapping, Routing, TelemetryHints,
129};
130pub use flow_resolve::{
131    ComponentSourceRefV1, FLOW_RESOLVE_SCHEMA_VERSION, FlowResolveV1, NodeResolveV1, ResolveModeV1,
132};
133#[cfg(all(feature = "std", feature = "serde"))]
134pub use flow_resolve::{read_flow_resolve, write_flow_resolve};
135#[cfg(feature = "std")]
136pub use flow_resolve::{sidecar_path_for_flow, validate_flow_resolve};
137pub use flow_resolve_summary::{
138    FLOW_RESOLVE_SUMMARY_SCHEMA_VERSION, FlowResolveSummaryManifestV1,
139    FlowResolveSummarySourceRefV1, FlowResolveSummaryV1, NodeResolveSummaryV1,
140};
141#[cfg(all(feature = "std", feature = "serde"))]
142pub use flow_resolve_summary::{read_flow_resolve_summary, write_flow_resolve_summary};
143#[cfg(feature = "std")]
144pub use flow_resolve_summary::{resolve_summary_path_for_flow, validate_flow_resolve_summary};
145pub use messaging::{Attachment, ChannelMessageEnvelope, MessageMetadata};
146pub use outcome::Outcome;
147pub use pack::extensions::component_manifests::{
148    ComponentManifestIndexEntryV1, ComponentManifestIndexError, ComponentManifestIndexV1,
149    EXT_COMPONENT_MANIFEST_INDEX_V1, ManifestEncoding,
150};
151#[cfg(feature = "serde")]
152pub use pack::extensions::component_manifests::{
153    decode_component_manifest_index_v1_from_cbor_bytes,
154    encode_component_manifest_index_v1_to_cbor_bytes,
155};
156pub use pack::extensions::component_sources::{
157    ArtifactLocationV1, ComponentSourceEntryV1, ComponentSourcesError, ComponentSourcesV1,
158    EXT_COMPONENT_SOURCES_V1, ResolvedComponentV1,
159};
160#[cfg(feature = "serde")]
161pub use pack::extensions::component_sources::{
162    decode_component_sources_v1_from_cbor_bytes, encode_component_sources_v1_to_cbor_bytes,
163};
164pub use pack::{PackRef, Signature, SignatureAlgorithm};
165pub use pack_manifest::{
166    BootstrapSpec, ComponentCapability, ExtensionInline, ExtensionRef, PackDependency,
167    PackFlowEntry, PackKind, PackManifest, PackSignatures,
168};
169pub use policy::{AllowList, NetworkPolicy, PolicyDecision, PolicyDecisionStatus, Protocol};
170pub use provider::{
171    PROVIDER_EXTENSION_ID, ProviderDecl, ProviderExtensionInline, ProviderManifest,
172    ProviderRuntimeRef,
173};
174#[cfg(feature = "time")]
175pub use run::RunResult;
176pub use run::{NodeFailure, NodeStatus, NodeSummary, RunStatus, TranscriptOffset};
177pub use secrets::{SecretFormat, SecretKey, SecretRequirement, SecretScope};
178pub use session::canonical_session_key;
179pub use session::{SessionCursor, SessionData, SessionKey};
180pub use state::{StateKey, StatePath};
181pub use store::{
182    ArtifactSelector, BundleSpec, CapabilityMap, Collection, ConnectionKind, DesiredState,
183    DesiredStateExportSpec, DesiredSubscriptionEntry, Environment, LayoutSection,
184    LayoutSectionKind, PackOrComponentRef, PlanLimits, PriceModel, ProductOverride, RolloutState,
185    RolloutStatus, StoreFront, StorePlan, StoreProduct, StoreProductKind, Subscription,
186    SubscriptionStatus, Theme, VersionStrategy,
187};
188pub use supply_chain::{
189    AttestationStatement, BuildPlan, BuildStatus, BuildStatusKind, MetadataRecord, PredicateType,
190    RepoContext, ScanKind, ScanRequest, ScanResult, ScanStatusKind, SignRequest, StoreContext,
191    VerifyRequest, VerifyResult,
192};
193#[cfg(feature = "otel-keys")]
194pub use telemetry::OtlpKeys;
195pub use telemetry::SpanContext;
196#[cfg(feature = "telemetry-autoinit")]
197pub use telemetry::TelemetryCtx;
198pub use tenant::{Impersonation, TenantIdentity};
199pub use tenant_config::{
200    DefaultPipeline, DidContext, DidService, DistributorTarget, EnabledPacks,
201    IdentityProviderOption, RepoAuth, RepoConfigFeatures, RepoSkin, RepoSkinLayout, RepoSkinLinks,
202    RepoSkinTheme, RepoTenantConfig, RepoWorkerPanel, StoreTarget, TenantDidDocument,
203    VerificationMethod,
204};
205pub use worker::{WorkerMessage, WorkerRequest, WorkerResponse};
206
207#[cfg(feature = "schemars")]
208use alloc::borrow::Cow;
209use alloc::{borrow::ToOwned, collections::BTreeMap, format, string::String, vec::Vec};
210use core::fmt;
211use core::str::FromStr;
212#[cfg(feature = "schemars")]
213use schemars::JsonSchema;
214use semver::VersionReq;
215#[cfg(feature = "time")]
216use time::OffsetDateTime;
217
218#[cfg(feature = "serde")]
219use serde::{Deserialize, Serialize};
220
221#[cfg(feature = "std")]
222use alloc::boxed::Box;
223
224#[cfg(feature = "std")]
225use std::error::Error as StdError;
226
227/// Validates identifiers to ensure they are non-empty and ASCII-safe.
228pub(crate) fn validate_identifier(value: &str, label: &str) -> GResult<()> {
229    if value.is_empty() {
230        return Err(GreenticError::new(
231            ErrorCode::InvalidInput,
232            format!("{label} must not be empty"),
233        ));
234    }
235    if value
236        .chars()
237        .any(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
238    {
239        return Err(GreenticError::new(
240            ErrorCode::InvalidInput,
241            format!("{label} must contain only ASCII letters, digits, '.', '-', or '_'"),
242        ));
243    }
244    Ok(())
245}
246
247/// Validates API key references that may include URI-like prefixes.
248pub(crate) fn validate_api_key_ref(value: &str) -> GResult<()> {
249    if value.trim().is_empty() {
250        return Err(GreenticError::new(
251            ErrorCode::InvalidInput,
252            "ApiKeyRef must not be empty",
253        ));
254    }
255    if value.chars().any(char::is_whitespace) {
256        return Err(GreenticError::new(
257            ErrorCode::InvalidInput,
258            "ApiKeyRef must not contain whitespace",
259        ));
260    }
261    if !value.is_ascii() {
262        return Err(GreenticError::new(
263            ErrorCode::InvalidInput,
264            "ApiKeyRef must contain only ASCII characters",
265        ));
266    }
267    Ok(())
268}
269
270/// Canonical schema IDs for the exported document types.
271pub mod ids {
272    /// Pack identifier schema.
273    pub const PACK_ID: &str =
274        "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-id.schema.json";
275    /// Component identifier schema.
276    pub const COMPONENT_ID: &str =
277        "https://greentic-ai.github.io/greentic-types/schemas/v1/component-id.schema.json";
278    /// Flow identifier schema.
279    pub const FLOW_ID: &str =
280        "https://greentic-ai.github.io/greentic-types/schemas/v1/flow-id.schema.json";
281    /// Node identifier schema.
282    pub const NODE_ID: &str =
283        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-id.schema.json";
284    /// Tenant context schema.
285    pub const TENANT_CONTEXT: &str =
286        "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-context.schema.json";
287    /// Hash digest schema.
288    pub const HASH_DIGEST: &str =
289        "https://greentic-ai.github.io/greentic-types/schemas/v1/hash-digest.schema.json";
290    /// Semantic version requirement schema.
291    pub const SEMVER_REQ: &str =
292        "https://greentic-ai.github.io/greentic-types/schemas/v1/semver-req.schema.json";
293    /// Redaction path schema.
294    pub const REDACTION_PATH: &str =
295        "https://greentic-ai.github.io/greentic-types/schemas/v1/redaction-path.schema.json";
296    /// Capabilities schema.
297    pub const CAPABILITIES: &str =
298        "https://greentic-ai.github.io/greentic-types/schemas/v1/capabilities.schema.json";
299    /// RepoSkin (skin.json) schema.
300    pub const REPO_SKIN: &str =
301        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-skin.schema.json";
302    /// RepoAuth (auth.json) schema.
303    pub const REPO_AUTH: &str =
304        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-auth.schema.json";
305    /// RepoTenantConfig (config.json) schema.
306    pub const REPO_TENANT_CONFIG: &str =
307        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-tenant-config.schema.json";
308    /// Tenant DID document (did.json) schema.
309    pub const TENANT_DID_DOCUMENT: &str =
310        "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-did-document.schema.json";
311    /// Flow schema.
312    pub const FLOW: &str = "greentic.flow.v1";
313    /// Flow resolve sidecar schema.
314    pub const FLOW_RESOLVE: &str = "greentic.flow.resolve.v1";
315    /// Flow resolve summary schema.
316    pub const FLOW_RESOLVE_SUMMARY: &str = "greentic.flow.resolve-summary.v1";
317    /// Node schema.
318    pub const NODE: &str =
319        "https://greentic-ai.github.io/greentic-types/schemas/v1/node.schema.json";
320    /// Component manifest schema.
321    pub const COMPONENT_MANIFEST: &str =
322        "https://greentic-ai.github.io/greentic-types/schemas/v1/component-manifest.schema.json";
323    /// Pack manifest schema.
324    pub const PACK_MANIFEST: &str = "greentic.pack-manifest.v1";
325    /// Provider manifest schema.
326    pub const PROVIDER_MANIFEST: &str =
327        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-manifest.schema.json";
328    /// Provider runtime reference schema.
329    pub const PROVIDER_RUNTIME_REF: &str =
330        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-runtime-ref.schema.json";
331    /// Provider declaration schema.
332    pub const PROVIDER_DECL: &str =
333        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-decl.schema.json";
334    /// Inline provider extension payload schema.
335    pub const PROVIDER_EXTENSION_INLINE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-extension-inline.schema.json";
336    /// Limits schema.
337    pub const LIMITS: &str =
338        "https://greentic-ai.github.io/greentic-types/schemas/v1/limits.schema.json";
339    /// Telemetry spec schema.
340    pub const TELEMETRY_SPEC: &str =
341        "https://greentic-ai.github.io/greentic-types/schemas/v1/telemetry-spec.schema.json";
342    /// Node summary schema.
343    pub const NODE_SUMMARY: &str =
344        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-summary.schema.json";
345    /// Node failure schema.
346    pub const NODE_FAILURE: &str =
347        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-failure.schema.json";
348    /// Node status schema.
349    pub const NODE_STATUS: &str =
350        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-status.schema.json";
351    /// Run status schema.
352    pub const RUN_STATUS: &str =
353        "https://greentic-ai.github.io/greentic-types/schemas/v1/run-status.schema.json";
354    /// Transcript offset schema.
355    pub const TRANSCRIPT_OFFSET: &str =
356        "https://greentic-ai.github.io/greentic-types/schemas/v1/transcript-offset.schema.json";
357    /// Tools capability schema.
358    pub const TOOLS_CAPS: &str =
359        "https://greentic-ai.github.io/greentic-types/schemas/v1/tools-caps.schema.json";
360    /// Secrets capability schema.
361    pub const SECRETS_CAPS: &str =
362        "https://greentic-ai.github.io/greentic-types/schemas/v1/secrets-caps.schema.json";
363    /// Branch reference schema.
364    pub const BRANCH_REF: &str =
365        "https://greentic-ai.github.io/greentic-types/schemas/v1/branch-ref.schema.json";
366    /// Commit reference schema.
367    pub const COMMIT_REF: &str =
368        "https://greentic-ai.github.io/greentic-types/schemas/v1/commit-ref.schema.json";
369    /// Git provider reference schema.
370    pub const GIT_PROVIDER_REF: &str =
371        "https://greentic-ai.github.io/greentic-types/schemas/v1/git-provider-ref.schema.json";
372    /// Scanner provider reference schema.
373    pub const SCANNER_REF: &str =
374        "https://greentic-ai.github.io/greentic-types/schemas/v1/scanner-ref.schema.json";
375    /// Webhook identifier schema.
376    pub const WEBHOOK_ID: &str =
377        "https://greentic-ai.github.io/greentic-types/schemas/v1/webhook-id.schema.json";
378    /// Repository reference schema.
379    pub const REPO_REF: &str =
380        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-ref.schema.json";
381    /// Component reference schema.
382    pub const COMPONENT_REF: &str =
383        "https://greentic-ai.github.io/greentic-types/schemas/v1/component-ref.schema.json";
384    /// Version reference schema.
385    pub const VERSION_REF: &str =
386        "https://greentic-ai.github.io/greentic-types/schemas/v1/version-ref.schema.json";
387    /// Build reference schema.
388    pub const BUILD_REF: &str =
389        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-ref.schema.json";
390    /// Scan reference schema.
391    pub const SCAN_REF: &str =
392        "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-ref.schema.json";
393    /// Attestation reference schema.
394    pub const ATTESTATION_REF: &str =
395        "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-ref.schema.json";
396    /// Attestation id schema.
397    pub const ATTESTATION_ID: &str =
398        "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-id.schema.json";
399    /// Policy reference schema.
400    pub const POLICY_REF: &str =
401        "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-ref.schema.json";
402    /// Policy input reference schema.
403    pub const POLICY_INPUT_REF: &str =
404        "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-input-ref.schema.json";
405    /// Store reference schema.
406    pub const STORE_REF: &str =
407        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-ref.schema.json";
408    /// Registry reference schema.
409    pub const REGISTRY_REF: &str =
410        "https://greentic-ai.github.io/greentic-types/schemas/v1/registry-ref.schema.json";
411    /// OCI image reference schema.
412    pub const OCI_IMAGE_REF: &str =
413        "https://greentic-ai.github.io/greentic-types/schemas/v1/oci-image-ref.schema.json";
414    /// Artifact reference schema.
415    pub const ARTIFACT_REF: &str =
416        "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-ref.schema.json";
417    /// SBOM reference schema.
418    pub const SBOM_REF: &str =
419        "https://greentic-ai.github.io/greentic-types/schemas/v1/sbom-ref.schema.json";
420    /// Signing key reference schema.
421    pub const SIGNING_KEY_REF: &str =
422        "https://greentic-ai.github.io/greentic-types/schemas/v1/signing-key-ref.schema.json";
423    /// Signature reference schema.
424    pub const SIGNATURE_REF: &str =
425        "https://greentic-ai.github.io/greentic-types/schemas/v1/signature-ref.schema.json";
426    /// Statement reference schema.
427    pub const STATEMENT_REF: &str =
428        "https://greentic-ai.github.io/greentic-types/schemas/v1/statement-ref.schema.json";
429    /// Build log reference schema.
430    pub const BUILD_LOG_REF: &str =
431        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-log-ref.schema.json";
432    /// Metadata record reference schema.
433    pub const METADATA_RECORD_REF: &str =
434        "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record-ref.schema.json";
435    /// API key reference schema.
436    pub const API_KEY_REF: &str =
437        "https://greentic-ai.github.io/greentic-types/schemas/v1/api-key-ref.schema.json";
438    /// Environment reference schema.
439    pub const ENVIRONMENT_REF: &str =
440        "https://greentic-ai.github.io/greentic-types/schemas/v1/environment-ref.schema.json";
441    /// Distributor reference schema.
442    pub const DISTRIBUTOR_REF: &str =
443        "https://greentic-ai.github.io/greentic-types/schemas/v1/distributor-ref.schema.json";
444    /// Storefront identifier schema.
445    pub const STOREFRONT_ID: &str =
446        "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront-id.schema.json";
447    /// Store product identifier schema.
448    pub const STORE_PRODUCT_ID: &str =
449        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-id.schema.json";
450    /// Store plan identifier schema.
451    pub const STORE_PLAN_ID: &str =
452        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan-id.schema.json";
453    /// Subscription identifier schema.
454    pub const SUBSCRIPTION_ID: &str =
455        "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-id.schema.json";
456    /// Bundle identifier schema.
457    pub const BUNDLE_ID: &str =
458        "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle-id.schema.json";
459    /// Collection identifier schema.
460    pub const COLLECTION_ID: &str =
461        "https://greentic-ai.github.io/greentic-types/schemas/v1/collection-id.schema.json";
462    /// Artifact selector schema.
463    pub const ARTIFACT_SELECTOR: &str =
464        "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-selector.schema.json";
465    /// Capability map schema.
466    pub const CAPABILITY_MAP: &str =
467        "https://greentic-ai.github.io/greentic-types/schemas/v1/capability-map.schema.json";
468    /// Store product kind schema.
469    pub const STORE_PRODUCT_KIND: &str =
470        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-kind.schema.json";
471    /// Version strategy schema.
472    pub const VERSION_STRATEGY: &str =
473        "https://greentic-ai.github.io/greentic-types/schemas/v1/version-strategy.schema.json";
474    /// Rollout status schema.
475    pub const ROLLOUT_STATUS: &str =
476        "https://greentic-ai.github.io/greentic-types/schemas/v1/rollout-status.schema.json";
477    /// Connection kind schema.
478    pub const CONNECTION_KIND: &str =
479        "https://greentic-ai.github.io/greentic-types/schemas/v1/connection-kind.schema.json";
480    /// Pack or component reference schema.
481    pub const PACK_OR_COMPONENT_REF: &str =
482        "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-or-component-ref.schema.json";
483    /// Plan limits schema.
484    pub const PLAN_LIMITS: &str =
485        "https://greentic-ai.github.io/greentic-types/schemas/v1/plan-limits.schema.json";
486    /// Price model schema.
487    pub const PRICE_MODEL: &str =
488        "https://greentic-ai.github.io/greentic-types/schemas/v1/price-model.schema.json";
489    /// Subscription status schema.
490    pub const SUBSCRIPTION_STATUS: &str =
491        "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-status.schema.json";
492    /// Build plan schema.
493    pub const BUILD_PLAN: &str =
494        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-plan.schema.json";
495    /// Build status schema.
496    pub const BUILD_STATUS: &str =
497        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-status.schema.json";
498    /// Scan request schema.
499    pub const SCAN_REQUEST: &str =
500        "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-request.schema.json";
501    /// Scan result schema.
502    pub const SCAN_RESULT: &str =
503        "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-result.schema.json";
504    /// Sign request schema.
505    pub const SIGN_REQUEST: &str =
506        "https://greentic-ai.github.io/greentic-types/schemas/v1/sign-request.schema.json";
507    /// Verify request schema.
508    pub const VERIFY_REQUEST: &str =
509        "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-request.schema.json";
510    /// Verify result schema.
511    pub const VERIFY_RESULT: &str =
512        "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-result.schema.json";
513    /// Attestation statement schema.
514    pub const ATTESTATION_STATEMENT: &str =
515        "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-statement.schema.json";
516    /// Metadata record schema.
517    pub const METADATA_RECORD: &str =
518        "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record.schema.json";
519    /// Repository context schema.
520    pub const REPO_CONTEXT: &str =
521        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-context.schema.json";
522    /// Store context schema.
523    pub const STORE_CONTEXT: &str =
524        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-context.schema.json";
525    /// Bundle schema.
526    pub const BUNDLE: &str =
527        "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle.schema.json";
528    /// Bundle export specification schema.
529    pub const DESIRED_STATE_EXPORT: &str =
530        "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state-export.schema.json";
531    /// Desired state schema.
532    pub const DESIRED_STATE: &str =
533        "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state.schema.json";
534    /// Desired subscription entry schema.
535    pub const DESIRED_SUBSCRIPTION_ENTRY: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-subscription-entry.schema.json";
536    /// Storefront schema.
537    pub const STOREFRONT: &str =
538        "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront.schema.json";
539    /// Store product schema.
540    pub const STORE_PRODUCT: &str =
541        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product.schema.json";
542    /// Store plan schema.
543    pub const STORE_PLAN: &str =
544        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan.schema.json";
545    /// Subscription schema.
546    pub const SUBSCRIPTION: &str =
547        "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription.schema.json";
548    /// Environment schema.
549    pub const ENVIRONMENT: &str =
550        "https://greentic-ai.github.io/greentic-types/schemas/v1/environment.schema.json";
551    /// Store theme schema.
552    pub const THEME: &str =
553        "https://greentic-ai.github.io/greentic-types/schemas/v1/theme.schema.json";
554    /// Layout section schema.
555    pub const LAYOUT_SECTION: &str =
556        "https://greentic-ai.github.io/greentic-types/schemas/v1/layout-section.schema.json";
557    /// Collection schema.
558    pub const COLLECTION: &str =
559        "https://greentic-ai.github.io/greentic-types/schemas/v1/collection.schema.json";
560    /// Product override schema.
561    pub const PRODUCT_OVERRIDE: &str =
562        "https://greentic-ai.github.io/greentic-types/schemas/v1/product-override.schema.json";
563    /// Event envelope schema.
564    pub const EVENT_ENVELOPE: &str =
565        "https://greentic-ai.github.io/greentic-types/schemas/v1/event-envelope.schema.json";
566    /// Event provider descriptor schema.
567    pub const EVENT_PROVIDER_DESCRIPTOR: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/event-provider-descriptor.schema.json";
568    /// Channel message envelope schema.
569    pub const CHANNEL_MESSAGE_ENVELOPE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/channel-message-envelope.schema.json";
570    /// Attachment schema.
571    pub const ATTACHMENT: &str =
572        "https://greentic-ai.github.io/greentic-types/schemas/v1/attachment.schema.json";
573    /// Worker request envelope schema.
574    pub const WORKER_REQUEST: &str =
575        "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-request.schema.json";
576    /// Worker message schema.
577    pub const WORKER_MESSAGE: &str =
578        "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-message.schema.json";
579    /// Worker response envelope schema.
580    pub const WORKER_RESPONSE: &str =
581        "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-response.schema.json";
582    /// OTLP attribute key schema.
583    pub const OTLP_KEYS: &str =
584        "https://greentic-ai.github.io/greentic-types/schemas/v1/otlp-keys.schema.json";
585    /// Run result schema.
586    pub const RUN_RESULT: &str =
587        "https://greentic-ai.github.io/greentic-types/schemas/v1/run-result.schema.json";
588}
589
590#[cfg(all(feature = "schema", feature = "std"))]
591/// Writes every JSON Schema to the provided directory.
592pub fn write_all_schemas(out_dir: &std::path::Path) -> anyhow::Result<()> {
593    use anyhow::Context;
594    use std::fs;
595
596    fs::create_dir_all(out_dir)
597        .with_context(|| format!("failed to create {}", out_dir.display()))?;
598
599    for entry in crate::schema::entries() {
600        let schema = (entry.generator)();
601        let path = out_dir.join(entry.file_name);
602        if let Some(parent) = path.parent() {
603            fs::create_dir_all(parent)
604                .with_context(|| format!("failed to create {}", parent.display()))?;
605        }
606
607        let json =
608            serde_json::to_vec_pretty(&schema).context("failed to serialize schema to JSON")?;
609        fs::write(&path, json).with_context(|| format!("failed to write {}", path.display()))?;
610    }
611
612    Ok(())
613}
614
615macro_rules! id_newtype {
616    ($name:ident, $doc:literal) => {
617        #[doc = $doc]
618        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
619        #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
620        #[cfg_attr(feature = "schemars", derive(JsonSchema))]
621        #[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
622        pub struct $name(pub String);
623
624        impl $name {
625            /// Returns the identifier as a string slice.
626            pub fn as_str(&self) -> &str {
627                &self.0
628            }
629
630            /// Validates and constructs the identifier from the provided value.
631            pub fn new(value: impl AsRef<str>) -> GResult<Self> {
632                value.as_ref().parse()
633            }
634        }
635
636        impl From<$name> for String {
637            fn from(value: $name) -> Self {
638                value.0
639            }
640        }
641
642        impl AsRef<str> for $name {
643            fn as_ref(&self) -> &str {
644                self.as_str()
645            }
646        }
647
648        impl fmt::Display for $name {
649            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650                f.write_str(self.as_str())
651            }
652        }
653
654        impl FromStr for $name {
655            type Err = GreenticError;
656
657            fn from_str(value: &str) -> Result<Self, Self::Err> {
658                validate_identifier(value, stringify!($name))?;
659                Ok(Self(value.to_owned()))
660            }
661        }
662
663        impl TryFrom<String> for $name {
664            type Error = GreenticError;
665
666            fn try_from(value: String) -> Result<Self, Self::Error> {
667                $name::from_str(&value)
668            }
669        }
670
671        impl TryFrom<&str> for $name {
672            type Error = GreenticError;
673
674            fn try_from(value: &str) -> Result<Self, Self::Error> {
675                $name::from_str(value)
676            }
677        }
678    };
679}
680
681id_newtype!(EnvId, "Environment identifier for a tenant context.");
682id_newtype!(TenantId, "Tenant identifier within an environment.");
683id_newtype!(TeamId, "Team identifier belonging to a tenant.");
684id_newtype!(UserId, "User identifier within a tenant.");
685id_newtype!(BranchRef, "Reference to a source control branch.");
686id_newtype!(CommitRef, "Reference to a source control commit.");
687id_newtype!(
688    GitProviderRef,
689    "Identifier referencing a source control provider."
690);
691id_newtype!(ScannerRef, "Identifier referencing a scanner provider.");
692id_newtype!(WebhookId, "Identifier referencing a registered webhook.");
693id_newtype!(PackId, "Globally unique pack identifier.");
694id_newtype!(
695    ComponentId,
696    "Identifier referencing a component binding in a pack."
697);
698id_newtype!(FlowId, "Identifier referencing a flow inside a pack.");
699id_newtype!(NodeId, "Identifier referencing a node inside a flow graph.");
700id_newtype!(
701    EnvironmentRef,
702    "Identifier referencing a deployment environment."
703);
704id_newtype!(
705    DistributorRef,
706    "Identifier referencing a distributor instance."
707);
708id_newtype!(StoreFrontId, "Identifier referencing a storefront.");
709id_newtype!(
710    StoreProductId,
711    "Identifier referencing a product in the store catalog."
712);
713id_newtype!(
714    StorePlanId,
715    "Identifier referencing a plan for a store product."
716);
717id_newtype!(
718    SubscriptionId,
719    "Identifier referencing a subscription entry."
720);
721id_newtype!(BundleId, "Identifier referencing a distributor bundle.");
722id_newtype!(CollectionId, "Identifier referencing a product collection.");
723id_newtype!(RepoRef, "Repository reference within a supply chain.");
724id_newtype!(
725    ComponentRef,
726    "Supply-chain component reference (distinct from pack ComponentId)."
727);
728id_newtype!(
729    VersionRef,
730    "Version reference for a component or metadata record."
731);
732id_newtype!(BuildRef, "Build reference within a supply chain.");
733id_newtype!(ScanRef, "Scan reference within a supply chain.");
734id_newtype!(
735    AttestationRef,
736    "Attestation reference within a supply chain."
737);
738id_newtype!(AttestationId, "Identifier referencing an attestation.");
739id_newtype!(PolicyRef, "Policy reference within a supply chain.");
740id_newtype!(
741    PolicyInputRef,
742    "Reference to a policy input payload for evaluation."
743);
744id_newtype!(StoreRef, "Content store reference within a supply chain.");
745id_newtype!(
746    RegistryRef,
747    "Registry reference for OCI or artifact storage."
748);
749id_newtype!(OciImageRef, "Reference to an OCI image for distribution.");
750id_newtype!(
751    ArtifactRef,
752    "Artifact reference within a build or scan result."
753);
754id_newtype!(
755    SbomRef,
756    "Reference to a Software Bill of Materials artifact."
757);
758id_newtype!(SigningKeyRef, "Reference to a signing key handle.");
759id_newtype!(SignatureRef, "Reference to a generated signature.");
760id_newtype!(StatementRef, "Reference to an attestation statement.");
761id_newtype!(
762    BuildLogRef,
763    "Reference to a build log output produced during execution."
764);
765id_newtype!(
766    MetadataRecordRef,
767    "Reference to a metadata record attached to artifacts or bundles."
768);
769
770/// API key reference used across secrets providers without exposing key material.
771#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
772#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
773#[cfg_attr(feature = "schemars", derive(JsonSchema))]
774#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
775pub struct ApiKeyRef(pub String);
776
777impl ApiKeyRef {
778    /// Returns the reference as a string slice.
779    pub fn as_str(&self) -> &str {
780        &self.0
781    }
782
783    /// Validates and constructs the reference from the provided value.
784    pub fn new(value: impl AsRef<str>) -> GResult<Self> {
785        value.as_ref().parse()
786    }
787}
788
789impl fmt::Display for ApiKeyRef {
790    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
791        f.write_str(self.as_str())
792    }
793}
794
795impl FromStr for ApiKeyRef {
796    type Err = GreenticError;
797
798    fn from_str(value: &str) -> Result<Self, Self::Err> {
799        validate_api_key_ref(value)?;
800        Ok(Self(value.to_owned()))
801    }
802}
803
804impl TryFrom<String> for ApiKeyRef {
805    type Error = GreenticError;
806
807    fn try_from(value: String) -> Result<Self, Self::Error> {
808        ApiKeyRef::from_str(&value)
809    }
810}
811
812impl TryFrom<&str> for ApiKeyRef {
813    type Error = GreenticError;
814
815    fn try_from(value: &str) -> Result<Self, Self::Error> {
816        ApiKeyRef::from_str(value)
817    }
818}
819
820impl From<ApiKeyRef> for String {
821    fn from(value: ApiKeyRef) -> Self {
822        value.0
823    }
824}
825
826impl AsRef<str> for ApiKeyRef {
827    fn as_ref(&self) -> &str {
828        self.as_str()
829    }
830}
831
832/// Compact tenant summary propagated to developers and tooling.
833#[derive(Clone, Debug, PartialEq, Eq, Hash)]
834#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
835#[cfg_attr(feature = "schemars", derive(JsonSchema))]
836pub struct TenantContext {
837    /// Tenant identifier owning the execution.
838    pub tenant_id: TenantId,
839    /// Optional team identifier scoped to the tenant.
840    #[cfg_attr(
841        feature = "serde",
842        serde(default, skip_serializing_if = "Option::is_none")
843    )]
844    pub team_id: Option<TeamId>,
845    /// Optional user identifier scoped to the tenant.
846    #[cfg_attr(
847        feature = "serde",
848        serde(default, skip_serializing_if = "Option::is_none")
849    )]
850    pub user_id: Option<UserId>,
851    /// Optional session identifier for end-to-end correlation.
852    #[cfg_attr(
853        feature = "serde",
854        serde(default, skip_serializing_if = "Option::is_none")
855    )]
856    pub session_id: Option<String>,
857    /// Optional attributes for routing and tracing.
858    #[cfg_attr(
859        feature = "serde",
860        serde(default, skip_serializing_if = "BTreeMap::is_empty")
861    )]
862    pub attributes: BTreeMap<String, String>,
863}
864
865impl TenantContext {
866    /// Creates a new tenant context scoped to the provided tenant id.
867    pub fn new(tenant_id: TenantId) -> Self {
868        Self {
869            tenant_id,
870            team_id: None,
871            user_id: None,
872            session_id: None,
873            attributes: BTreeMap::new(),
874        }
875    }
876}
877
878impl From<&TenantCtx> for TenantContext {
879    fn from(ctx: &TenantCtx) -> Self {
880        Self {
881            tenant_id: ctx.tenant_id.clone(),
882            team_id: ctx.team_id.clone().or_else(|| ctx.team.clone()),
883            user_id: ctx.user_id.clone().or_else(|| ctx.user.clone()),
884            session_id: ctx.session_id.clone(),
885            attributes: ctx.attributes.clone(),
886        }
887    }
888}
889
890/// Supported hashing algorithms for pack content digests.
891#[derive(Clone, Debug, PartialEq, Eq, Hash)]
892#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
893#[cfg_attr(feature = "schemars", derive(JsonSchema))]
894#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
895pub enum HashAlgorithm {
896    /// Blake3 hashing algorithm.
897    Blake3,
898    /// Catch all for other algorithms.
899    Other(String),
900}
901
902/// Content digest describing a pack or artifact.
903#[derive(Clone, Debug, PartialEq, Eq, Hash)]
904#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
905#[cfg_attr(
906    feature = "serde",
907    serde(into = "HashDigestRepr", try_from = "HashDigestRepr")
908)]
909#[cfg_attr(feature = "schemars", derive(JsonSchema))]
910pub struct HashDigest {
911    /// Hash algorithm used to produce the digest.
912    pub algo: HashAlgorithm,
913    /// Hex encoded digest bytes.
914    pub hex: String,
915}
916
917impl HashDigest {
918    /// Creates a new digest ensuring the hex payload is valid.
919    pub fn new(algo: HashAlgorithm, hex: impl Into<String>) -> GResult<Self> {
920        let hex = hex.into();
921        validate_hex(&hex)?;
922        Ok(Self { algo, hex })
923    }
924
925    /// Convenience constructor for Blake3 digests.
926    pub fn blake3(hex: impl Into<String>) -> GResult<Self> {
927        Self::new(HashAlgorithm::Blake3, hex)
928    }
929}
930
931#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
932#[cfg_attr(feature = "schemars", derive(JsonSchema))]
933struct HashDigestRepr {
934    algo: HashAlgorithm,
935    hex: String,
936}
937
938impl From<HashDigest> for HashDigestRepr {
939    fn from(value: HashDigest) -> Self {
940        Self {
941            algo: value.algo,
942            hex: value.hex,
943        }
944    }
945}
946
947impl TryFrom<HashDigestRepr> for HashDigest {
948    type Error = GreenticError;
949
950    fn try_from(value: HashDigestRepr) -> Result<Self, Self::Error> {
951        HashDigest::new(value.algo, value.hex)
952    }
953}
954
955fn validate_hex(hex: &str) -> GResult<()> {
956    if hex.is_empty() {
957        return Err(GreenticError::new(
958            ErrorCode::InvalidInput,
959            "digest hex payload must not be empty",
960        ));
961    }
962    if hex.len() % 2 != 0 {
963        return Err(GreenticError::new(
964            ErrorCode::InvalidInput,
965            "digest hex payload must have an even number of digits",
966        ));
967    }
968    if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
969        return Err(GreenticError::new(
970            ErrorCode::InvalidInput,
971            "digest hex payload must be hexadecimal",
972        ));
973    }
974    Ok(())
975}
976
977/// Semantic version requirement validated by [`semver`].
978#[derive(Clone, Debug, PartialEq, Eq, Hash)]
979#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
980#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
981pub struct SemverReq(String);
982
983impl SemverReq {
984    /// Parses and validates a semantic version requirement string.
985    pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
986        let value = value.as_ref();
987        VersionReq::parse(value).map_err(|err| {
988            GreenticError::new(
989                ErrorCode::InvalidInput,
990                format!("invalid semver requirement '{value}': {err}"),
991            )
992        })?;
993        Ok(Self(value.to_owned()))
994    }
995
996    /// Returns the underlying string slice.
997    pub fn as_str(&self) -> &str {
998        &self.0
999    }
1000
1001    /// Converts into a [`semver::VersionReq`].
1002    pub fn to_version_req(&self) -> VersionReq {
1003        VersionReq::parse(&self.0)
1004            .unwrap_or_else(|err| unreachable!("SemverReq::parse validated inputs: {err}"))
1005    }
1006}
1007
1008impl fmt::Display for SemverReq {
1009    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1010        f.write_str(self.as_str())
1011    }
1012}
1013
1014impl From<SemverReq> for String {
1015    fn from(value: SemverReq) -> Self {
1016        value.0
1017    }
1018}
1019
1020impl TryFrom<String> for SemverReq {
1021    type Error = GreenticError;
1022
1023    fn try_from(value: String) -> Result<Self, Self::Error> {
1024        SemverReq::parse(&value)
1025    }
1026}
1027
1028impl TryFrom<&str> for SemverReq {
1029    type Error = GreenticError;
1030
1031    fn try_from(value: &str) -> Result<Self, Self::Error> {
1032        SemverReq::parse(value)
1033    }
1034}
1035
1036impl FromStr for SemverReq {
1037    type Err = GreenticError;
1038
1039    fn from_str(s: &str) -> Result<Self, Self::Err> {
1040        SemverReq::parse(s)
1041    }
1042}
1043
1044/// JSONPath expression pointing at sensitive fields that should be redacted.
1045#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1046#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1047#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1048pub struct RedactionPath(String);
1049
1050impl RedactionPath {
1051    /// Validates and stores a JSONPath expression.
1052    pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1053        let value = value.as_ref();
1054        validate_jsonpath(value)?;
1055        Ok(Self(value.to_owned()))
1056    }
1057
1058    /// Returns the JSONPath string.
1059    pub fn as_str(&self) -> &str {
1060        &self.0
1061    }
1062}
1063
1064impl fmt::Display for RedactionPath {
1065    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1066        f.write_str(self.as_str())
1067    }
1068}
1069
1070impl From<RedactionPath> for String {
1071    fn from(value: RedactionPath) -> Self {
1072        value.0
1073    }
1074}
1075
1076impl TryFrom<String> for RedactionPath {
1077    type Error = GreenticError;
1078
1079    fn try_from(value: String) -> Result<Self, Self::Error> {
1080        RedactionPath::parse(&value)
1081    }
1082}
1083
1084impl TryFrom<&str> for RedactionPath {
1085    type Error = GreenticError;
1086
1087    fn try_from(value: &str) -> Result<Self, Self::Error> {
1088        RedactionPath::parse(value)
1089    }
1090}
1091
1092fn validate_jsonpath(path: &str) -> GResult<()> {
1093    if path.is_empty() {
1094        return Err(GreenticError::new(
1095            ErrorCode::InvalidInput,
1096            "redaction path cannot be empty",
1097        ));
1098    }
1099    if !path.starts_with('$') {
1100        return Err(GreenticError::new(
1101            ErrorCode::InvalidInput,
1102            "redaction path must start with '$'",
1103        ));
1104    }
1105    if path.chars().any(|c| c.is_control()) {
1106        return Err(GreenticError::new(
1107            ErrorCode::InvalidInput,
1108            "redaction path cannot contain control characters",
1109        ));
1110    }
1111    Ok(())
1112}
1113
1114#[cfg(feature = "schemars")]
1115impl JsonSchema for SemverReq {
1116    fn schema_name() -> Cow<'static, str> {
1117        Cow::Borrowed("SemverReq")
1118    }
1119
1120    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1121        let mut schema = <String>::json_schema(generator);
1122        if schema.get("description").is_none() {
1123            schema.insert(
1124                "description".into(),
1125                "Validated semantic version requirement string".into(),
1126            );
1127        }
1128        schema
1129    }
1130}
1131
1132#[cfg(feature = "schemars")]
1133impl JsonSchema for RedactionPath {
1134    fn schema_name() -> Cow<'static, str> {
1135        Cow::Borrowed("RedactionPath")
1136    }
1137
1138    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1139        let mut schema = <String>::json_schema(generator);
1140        if schema.get("description").is_none() {
1141            schema.insert(
1142                "description".into(),
1143                "JSONPath expression used for runtime redaction".into(),
1144            );
1145        }
1146        schema
1147    }
1148}
1149
1150/// Deadline metadata for an invocation, stored as Unix epoch milliseconds.
1151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1152#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1153#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1154pub struct InvocationDeadline {
1155    unix_millis: i128,
1156}
1157
1158impl InvocationDeadline {
1159    /// Creates a deadline from a Unix timestamp expressed in milliseconds.
1160    pub const fn from_unix_millis(unix_millis: i128) -> Self {
1161        Self { unix_millis }
1162    }
1163
1164    /// Returns the deadline as Unix epoch milliseconds.
1165    pub const fn unix_millis(&self) -> i128 {
1166        self.unix_millis
1167    }
1168
1169    /// Converts the deadline into an [`OffsetDateTime`].
1170    #[cfg(feature = "time")]
1171    pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, time::error::ComponentRange> {
1172        OffsetDateTime::from_unix_timestamp_nanos(self.unix_millis * 1_000_000)
1173    }
1174
1175    /// Creates a deadline from an [`OffsetDateTime`], truncating to milliseconds.
1176    #[cfg(feature = "time")]
1177    pub fn from_offset_date_time(value: OffsetDateTime) -> Self {
1178        let nanos = value.unix_timestamp_nanos();
1179        Self {
1180            unix_millis: nanos / 1_000_000,
1181        }
1182    }
1183}
1184
1185/// Context that accompanies every invocation across Greentic runtimes.
1186#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1187#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1188#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1189pub struct TenantCtx {
1190    /// Environment scope (for example `dev`, `staging`, or `prod`).
1191    pub env: EnvId,
1192    /// Tenant identifier for the current execution.
1193    pub tenant: TenantId,
1194    /// Stable tenant identifier reference used across systems.
1195    pub tenant_id: TenantId,
1196    /// Optional team identifier scoped to the tenant.
1197    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1198    pub team: Option<TeamId>,
1199    /// Optional team identifier accessible via the shared schema.
1200    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1201    pub team_id: Option<TeamId>,
1202    /// Optional user identifier scoped to the tenant.
1203    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1204    pub user: Option<UserId>,
1205    /// Optional user identifier aligned with the shared schema.
1206    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1207    pub user_id: Option<UserId>,
1208    /// Optional session identifier propagated by the runtime.
1209    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1210    pub session_id: Option<String>,
1211    /// Optional flow identifier for the current execution.
1212    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1213    pub flow_id: Option<String>,
1214    /// Optional node identifier within the flow.
1215    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1216    pub node_id: Option<String>,
1217    /// Optional provider identifier describing the runtime surface.
1218    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1219    pub provider_id: Option<String>,
1220    /// Distributed tracing identifier when available.
1221    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1222    pub trace_id: Option<String>,
1223    /// Correlation identifier for linking related events.
1224    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1225    pub correlation_id: Option<String>,
1226    /// Free-form attributes for routing and tracing.
1227    #[cfg_attr(
1228        feature = "serde",
1229        serde(default, skip_serializing_if = "BTreeMap::is_empty")
1230    )]
1231    pub attributes: BTreeMap<String, String>,
1232    /// Deadline when the invocation should finish.
1233    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1234    pub deadline: Option<InvocationDeadline>,
1235    /// Attempt counter for retried invocations (starting at zero).
1236    pub attempt: u32,
1237    /// Stable idempotency key propagated across retries.
1238    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1239    pub idempotency_key: Option<String>,
1240    /// Optional impersonation context describing the acting identity.
1241    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1242    pub impersonation: Option<Impersonation>,
1243}
1244
1245impl TenantCtx {
1246    /// Creates a new tenant context with the provided environment and tenant identifiers.
1247    pub fn new(env: EnvId, tenant: TenantId) -> Self {
1248        let tenant_id = tenant.clone();
1249        Self {
1250            env,
1251            tenant: tenant.clone(),
1252            tenant_id,
1253            team: None,
1254            team_id: None,
1255            user: None,
1256            user_id: None,
1257            session_id: None,
1258            flow_id: None,
1259            node_id: None,
1260            provider_id: None,
1261            trace_id: None,
1262            correlation_id: None,
1263            attributes: BTreeMap::new(),
1264            deadline: None,
1265            attempt: 0,
1266            idempotency_key: None,
1267            impersonation: None,
1268        }
1269    }
1270
1271    /// Updates the team information ensuring legacy and shared fields stay aligned.
1272    pub fn with_team(mut self, team: Option<TeamId>) -> Self {
1273        self.team = team.clone();
1274        self.team_id = team;
1275        self
1276    }
1277
1278    /// Updates the user information ensuring legacy and shared fields stay aligned.
1279    pub fn with_user(mut self, user: Option<UserId>) -> Self {
1280        self.user = user.clone();
1281        self.user_id = user;
1282        self
1283    }
1284
1285    /// Updates the session identifier.
1286    pub fn with_session(mut self, session: impl Into<String>) -> Self {
1287        self.session_id = Some(session.into());
1288        self
1289    }
1290
1291    /// Updates the flow identifier.
1292    pub fn with_flow(mut self, flow: impl Into<String>) -> Self {
1293        self.flow_id = Some(flow.into());
1294        self
1295    }
1296
1297    /// Updates the node identifier.
1298    pub fn with_node(mut self, node: impl Into<String>) -> Self {
1299        self.node_id = Some(node.into());
1300        self
1301    }
1302
1303    /// Updates the provider identifier.
1304    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1305        self.provider_id = Some(provider.into());
1306        self
1307    }
1308
1309    /// Attaches or replaces the attributes map.
1310    pub fn with_attributes(mut self, attributes: BTreeMap<String, String>) -> Self {
1311        self.attributes = attributes;
1312        self
1313    }
1314
1315    /// Sets the impersonation context.
1316    pub fn with_impersonation(mut self, impersonation: Option<Impersonation>) -> Self {
1317        self.impersonation = impersonation;
1318        self
1319    }
1320
1321    /// Returns a copy of the context with the provided attempt value.
1322    pub fn with_attempt(mut self, attempt: u32) -> Self {
1323        self.attempt = attempt;
1324        self
1325    }
1326
1327    /// Updates the deadline metadata for subsequent invocations.
1328    pub fn with_deadline(mut self, deadline: Option<InvocationDeadline>) -> Self {
1329        self.deadline = deadline;
1330        self
1331    }
1332
1333    /// Returns the session identifier, when present.
1334    pub fn session_id(&self) -> Option<&str> {
1335        self.session_id.as_deref()
1336    }
1337
1338    /// Returns the flow identifier, when present.
1339    pub fn flow_id(&self) -> Option<&str> {
1340        self.flow_id.as_deref()
1341    }
1342
1343    /// Returns the node identifier, when present.
1344    pub fn node_id(&self) -> Option<&str> {
1345        self.node_id.as_deref()
1346    }
1347
1348    /// Returns the provider identifier, when present.
1349    pub fn provider_id(&self) -> Option<&str> {
1350        self.provider_id.as_deref()
1351    }
1352}
1353
1354/// Primary payload representation shared across envelopes.
1355pub type BinaryPayload = Vec<u8>;
1356
1357/// Normalized ingress payload delivered to nodes.
1358#[derive(Clone, Debug, PartialEq, Eq)]
1359#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1360#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1361pub struct InvocationEnvelope {
1362    /// Tenant context for the invocation.
1363    pub ctx: TenantCtx,
1364    /// Flow identifier the event belongs to.
1365    pub flow_id: String,
1366    /// Optional node identifier within the flow.
1367    pub node_id: Option<String>,
1368    /// Operation being invoked (for example `on_message` or `tick`).
1369    pub op: String,
1370    /// Normalized payload for the invocation.
1371    pub payload: BinaryPayload,
1372    /// Raw metadata propagated from the ingress surface.
1373    pub metadata: BinaryPayload,
1374}
1375
1376/// Structured detail payload attached to a node error.
1377#[derive(Clone, Debug, PartialEq, Eq)]
1378#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1379#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1380pub enum ErrorDetail {
1381    /// UTF-8 encoded detail payload.
1382    Text(String),
1383    /// Binary payload detail (for example message pack or CBOR).
1384    Binary(BinaryPayload),
1385}
1386
1387/// Error type emitted by Greentic nodes.
1388#[derive(Debug)]
1389#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1390#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1391pub struct NodeError {
1392    /// Machine readable error code.
1393    pub code: String,
1394    /// Human readable message explaining the failure.
1395    pub message: String,
1396    /// Whether the failure is retryable by the runtime.
1397    pub retryable: bool,
1398    /// Optional backoff duration in milliseconds for the next retry.
1399    pub backoff_ms: Option<u64>,
1400    /// Optional structured error detail payload.
1401    pub details: Option<ErrorDetail>,
1402    #[cfg(feature = "std")]
1403    #[cfg_attr(feature = "serde", serde(skip, default = "default_source"))]
1404    #[cfg_attr(feature = "schemars", schemars(skip))]
1405    source: Option<Box<dyn StdError + Send + Sync>>,
1406}
1407
1408#[cfg(all(feature = "std", feature = "serde"))]
1409fn default_source() -> Option<Box<dyn StdError + Send + Sync>> {
1410    None
1411}
1412
1413impl NodeError {
1414    /// Constructs a non-retryable failure with the supplied code and message.
1415    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
1416        Self {
1417            code: code.into(),
1418            message: message.into(),
1419            retryable: false,
1420            backoff_ms: None,
1421            details: None,
1422            #[cfg(feature = "std")]
1423            source: None,
1424        }
1425    }
1426
1427    /// Marks the error as retryable with an optional backoff value.
1428    pub fn with_retry(mut self, backoff_ms: Option<u64>) -> Self {
1429        self.retryable = true;
1430        self.backoff_ms = backoff_ms;
1431        self
1432    }
1433
1434    /// Attaches structured details to the error.
1435    pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
1436        self.details = Some(detail);
1437        self
1438    }
1439
1440    /// Attaches a textual detail payload to the error.
1441    pub fn with_detail_text(mut self, detail: impl Into<String>) -> Self {
1442        self.details = Some(ErrorDetail::Text(detail.into()));
1443        self
1444    }
1445
1446    /// Attaches a binary detail payload to the error.
1447    pub fn with_detail_binary(mut self, detail: BinaryPayload) -> Self {
1448        self.details = Some(ErrorDetail::Binary(detail));
1449        self
1450    }
1451
1452    #[cfg(feature = "std")]
1453    /// Attaches a source error to the failure for debugging purposes.
1454    pub fn with_source<E>(mut self, source: E) -> Self
1455    where
1456        E: StdError + Send + Sync + 'static,
1457    {
1458        self.source = Some(Box::new(source));
1459        self
1460    }
1461
1462    /// Returns the structured details, when available.
1463    pub fn detail(&self) -> Option<&ErrorDetail> {
1464        self.details.as_ref()
1465    }
1466
1467    #[cfg(feature = "std")]
1468    /// Returns the attached source error when one has been provided.
1469    pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
1470        self.source.as_deref()
1471    }
1472}
1473
1474impl fmt::Display for NodeError {
1475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1476        write!(f, "{}: {}", self.code, self.message)
1477    }
1478}
1479
1480#[cfg(feature = "std")]
1481impl StdError for NodeError {
1482    fn source(&self) -> Option<&(dyn StdError + 'static)> {
1483        self.source
1484            .as_ref()
1485            .map(|err| err.as_ref() as &(dyn StdError + 'static))
1486    }
1487}
1488
1489/// Alias for results returned by node handlers.
1490pub type NodeResult<T> = Result<T, NodeError>;
1491
1492/// Generates a stable idempotency key for a node invocation.
1493///
1494/// The key uses tenant, flow, node, and correlation identifiers. Missing
1495/// correlation values fall back to the value stored on the context.
1496pub fn make_idempotency_key(
1497    ctx: &TenantCtx,
1498    flow_id: &str,
1499    node_id: Option<&str>,
1500    correlation: Option<&str>,
1501) -> String {
1502    let node_segment = node_id.unwrap_or_default();
1503    let correlation_segment = correlation
1504        .or(ctx.correlation_id.as_deref())
1505        .unwrap_or_default();
1506    let input = format!(
1507        "{}|{}|{}|{}",
1508        ctx.tenant_id.as_str(),
1509        flow_id,
1510        node_segment,
1511        correlation_segment
1512    );
1513    fnv1a_128_hex(input.as_bytes())
1514}
1515
1516const FNV_OFFSET_BASIS: u128 = 0x6c62272e07bb014262b821756295c58d;
1517const FNV_PRIME: u128 = 0x0000000001000000000000000000013b;
1518
1519fn fnv1a_128_hex(bytes: &[u8]) -> String {
1520    let mut hash = FNV_OFFSET_BASIS;
1521    for &byte in bytes {
1522        hash ^= byte as u128;
1523        hash = hash.wrapping_mul(FNV_PRIME);
1524    }
1525
1526    let mut output = String::with_capacity(32);
1527    for shift in (0..32).rev() {
1528        let nibble = ((hash >> (shift * 4)) & 0x0f) as u8;
1529        output.push(match nibble {
1530            0..=9 => (b'0' + nibble) as char,
1531            _ => (b'a' + (nibble - 10)) as char,
1532        });
1533    }
1534    output
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539    use super::*;
1540    use core::convert::TryFrom;
1541    use time::OffsetDateTime;
1542
1543    fn sample_ctx() -> TenantCtx {
1544        let env = EnvId::try_from("prod").unwrap_or_else(|err| panic!("{err}"));
1545        let tenant = TenantId::try_from("tenant-123").unwrap_or_else(|err| panic!("{err}"));
1546        let team = TeamId::try_from("team-456").unwrap_or_else(|err| panic!("{err}"));
1547        let user = UserId::try_from("user-789").unwrap_or_else(|err| panic!("{err}"));
1548
1549        let mut ctx = TenantCtx::new(env, tenant)
1550            .with_team(Some(team))
1551            .with_user(Some(user))
1552            .with_attempt(2)
1553            .with_deadline(Some(InvocationDeadline::from_unix_millis(
1554                1_700_000_000_000,
1555            )));
1556        ctx.trace_id = Some("trace-abc".to_owned());
1557        ctx.correlation_id = Some("corr-xyz".to_owned());
1558        ctx.idempotency_key = Some("key-123".to_owned());
1559        ctx
1560    }
1561
1562    #[test]
1563    fn idempotent_key_stable() {
1564        let ctx = sample_ctx();
1565        let key_a = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1566        let key_b = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1567        assert_eq!(key_a, key_b);
1568        assert_eq!(key_a.len(), 32);
1569    }
1570
1571    #[test]
1572    fn idempotent_key_uses_context_correlation() {
1573        let ctx = sample_ctx();
1574        let key = make_idempotency_key(&ctx, "flow-1", None, None);
1575        let expected = make_idempotency_key(&ctx, "flow-1", None, ctx.correlation_id.as_deref());
1576        assert_eq!(key, expected);
1577    }
1578
1579    #[test]
1580    #[cfg(feature = "time")]
1581    fn deadline_roundtrips_through_offset_datetime() {
1582        let dt = OffsetDateTime::from_unix_timestamp(1_700_000_000)
1583            .unwrap_or_else(|err| panic!("valid timestamp: {err}"));
1584        let deadline = InvocationDeadline::from_offset_date_time(dt);
1585        let roundtrip = deadline
1586            .to_offset_date_time()
1587            .unwrap_or_else(|err| panic!("round-trip conversion failed: {err}"));
1588        let millis = dt.unix_timestamp_nanos() / 1_000_000;
1589        assert_eq!(deadline.unix_millis(), millis);
1590        assert_eq!(roundtrip.unix_timestamp_nanos() / 1_000_000, millis);
1591    }
1592
1593    #[test]
1594    fn node_error_builder_sets_fields() {
1595        let err = NodeError::new("TEST", "example")
1596            .with_retry(Some(500))
1597            .with_detail_text("context");
1598
1599        assert!(err.retryable);
1600        assert_eq!(err.backoff_ms, Some(500));
1601        match err.detail() {
1602            Some(ErrorDetail::Text(detail)) => assert_eq!(detail, "context"),
1603            other => panic!("unexpected detail {other:?}"),
1604        }
1605    }
1606
1607    #[cfg(feature = "std")]
1608    #[test]
1609    fn node_error_source_roundtrips() {
1610        use std::io::Error;
1611
1612        let source = Error::other("boom");
1613        let err = NodeError::new("TEST", "example").with_source(source);
1614        assert!(err.source().is_some());
1615    }
1616}