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