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