Skip to main content

greentic_types/
lib.rs

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