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