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