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