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