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