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