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