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