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