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