Skip to main content

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