Skip to main content

greentic_types/
lib.rs

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