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