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 universal_dto::{
168 AuthUserRefV1, EncodeInV1, Header, HttpInV1, HttpOutV1, ProviderPayloadV1, RenderPlanInV1,
169 RenderPlanOutV1, SendPayloadInV1, SendPayloadResultV1, SubscriptionDeleteInV1,
170 SubscriptionDeleteOutV1, SubscriptionDeleteResultV1, SubscriptionEnsureInV1,
171 SubscriptionEnsureOutV1, SubscriptionEnsureResultV1, SubscriptionRenewInV1,
172 SubscriptionRenewOutV1, SubscriptionRenewalInV1, SubscriptionRenewalOutV1,
173 },
174};
175pub use op_descriptor::{IoSchema, OpDescriptor, OpExample};
176pub use outcome::Outcome;
177pub use pack::extensions::component_manifests::{
178 ComponentManifestIndexEntryV1, ComponentManifestIndexError, ComponentManifestIndexV1,
179 EXT_COMPONENT_MANIFEST_INDEX_V1, ManifestEncoding,
180};
181#[cfg(feature = "serde")]
182pub use pack::extensions::component_manifests::{
183 decode_component_manifest_index_v1_from_cbor_bytes,
184 encode_component_manifest_index_v1_to_cbor_bytes,
185};
186pub use pack::extensions::component_sources::{
187 ArtifactLocationV1, ComponentSourceEntryV1, ComponentSourcesError, ComponentSourcesV1,
188 EXT_COMPONENT_SOURCES_V1, ResolvedComponentV1,
189};
190#[cfg(feature = "serde")]
191pub use pack::extensions::component_sources::{
192 decode_component_sources_v1_from_cbor_bytes, encode_component_sources_v1_to_cbor_bytes,
193};
194pub use pack::{PackRef, Signature, SignatureAlgorithm};
195pub use pack_manifest::{
196 BootstrapSpec, ComponentCapability, ExtensionInline, ExtensionRef, PackDependency,
197 PackFlowEntry, PackKind, PackManifest, PackSignatures,
198};
199pub use policy::{AllowList, NetworkPolicy, PolicyDecision, PolicyDecisionStatus, Protocol};
200pub use provider::{
201 PROVIDER_EXTENSION_ID, ProviderDecl, ProviderExtensionInline, ProviderManifest,
202 ProviderRuntimeRef,
203};
204pub use provider_install::{ProviderInstallRecord, ProviderInstallRefs};
205pub use qa::{
206 CanonicalPolicy, ExampleAnswers, QaSpecSource, SetupContract, SetupOutput, validate_answers,
207};
208#[cfg(feature = "time")]
209pub use run::RunResult;
210pub use run::{NodeFailure, NodeStatus, NodeSummary, RunStatus, TranscriptOffset};
211pub use schema_id::{IoSchemaSource, QaSchemaSource, SchemaId, SchemaSource, schema_id_for_cbor};
212pub use schema_registry::{SCHEMAS, SchemaDef};
213pub use schemas::component::v0_5_0::LegacyComponentQaSpec;
214pub use schemas::component::v0_6_0::{
215 ComponentDescribe, ComponentInfo, ComponentQaSpec, ComponentRunInput, ComponentRunOutput,
216 QaMode as ComponentQaMode, Question as ComponentQuestion,
217 QuestionKind as ComponentQuestionKind,
218};
219pub use schemas::pack::v0_6_0::{
220 CapabilityDescriptor, CapabilityMetadata, PackDescribe, PackInfo, PackQaSpec,
221 PackValidationResult,
222};
223pub use secrets::{SecretFormat, SecretKey, SecretRequirement, SecretScope};
224pub use session::canonical_session_key;
225pub use session::{ReplyScope, SessionCursor, SessionData, SessionKey, WaitScope};
226pub use state::{StateKey, StatePath};
227pub use store::{
228 ArtifactSelector, BundleSpec, CapabilityMap, Collection, ConnectionKind, DesiredState,
229 DesiredStateExportSpec, DesiredSubscriptionEntry, Environment, LayoutSection,
230 LayoutSectionKind, PackOrComponentRef, PlanLimits, PriceModel, ProductOverride, RolloutState,
231 RolloutStatus, StoreFront, StorePlan, StoreProduct, StoreProductKind, Subscription,
232 SubscriptionStatus, Theme, VersionStrategy,
233};
234pub use supply_chain::{
235 AttestationStatement, BuildPlan, BuildStatus, BuildStatusKind, MetadataRecord, PredicateType,
236 RepoContext, ScanKind, ScanRequest, ScanResult, ScanStatusKind, SignRequest, StoreContext,
237 VerifyRequest, VerifyResult,
238};
239#[cfg(feature = "otel-keys")]
240pub use telemetry::OtlpKeys;
241pub use telemetry::SpanContext;
242#[cfg(feature = "telemetry-autoinit")]
243pub use telemetry::TelemetryCtx;
244pub use tenant::{Impersonation, TenantIdentity};
245pub use tenant_config::{
246 DefaultPipeline, DidContext, DidService, DistributorTarget, EnabledPacks,
247 IdentityProviderOption, RepoAuth, RepoConfigFeatures, RepoSkin, RepoSkinLayout, RepoSkinLinks,
248 RepoSkinTheme, RepoTenantConfig, RepoWorkerPanel, StoreTarget, TenantDidDocument,
249 VerificationMethod,
250};
251pub use validate::{
252 Diagnostic, PackValidator, Severity, ValidationCounts, ValidationReport,
253 validate_pack_manifest_core,
254};
255pub use worker::{WorkerMessage, WorkerRequest, WorkerResponse};
256
257#[cfg(feature = "schemars")]
258use alloc::borrow::Cow;
259use alloc::{borrow::ToOwned, collections::BTreeMap, format, string::String, vec::Vec};
260use core::fmt;
261use core::str::FromStr;
262#[cfg(feature = "schemars")]
263use schemars::JsonSchema;
264use semver::VersionReq;
265#[cfg(feature = "time")]
266use time::OffsetDateTime;
267
268#[cfg(feature = "serde")]
269use serde::{Deserialize, Serialize};
270
271#[cfg(feature = "std")]
272use alloc::boxed::Box;
273
274#[cfg(feature = "std")]
275use std::error::Error as StdError;
276
277pub(crate) fn validate_identifier(value: &str, label: &str) -> GResult<()> {
279 if value.is_empty() {
280 return Err(GreenticError::new(
281 ErrorCode::InvalidInput,
282 format!("{label} must not be empty"),
283 ));
284 }
285 if value
286 .chars()
287 .any(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
288 {
289 return Err(GreenticError::new(
290 ErrorCode::InvalidInput,
291 format!("{label} must contain only ASCII letters, digits, '.', '-', or '_'"),
292 ));
293 }
294 Ok(())
295}
296
297pub(crate) fn validate_api_key_ref(value: &str) -> GResult<()> {
299 if value.trim().is_empty() {
300 return Err(GreenticError::new(
301 ErrorCode::InvalidInput,
302 "ApiKeyRef must not be empty",
303 ));
304 }
305 if value.chars().any(char::is_whitespace) {
306 return Err(GreenticError::new(
307 ErrorCode::InvalidInput,
308 "ApiKeyRef must not contain whitespace",
309 ));
310 }
311 if !value.is_ascii() {
312 return Err(GreenticError::new(
313 ErrorCode::InvalidInput,
314 "ApiKeyRef must contain only ASCII characters",
315 ));
316 }
317 Ok(())
318}
319
320pub mod ids {
322 pub const PACK_ID: &str =
324 "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-id.schema.json";
325 pub const COMPONENT_ID: &str =
327 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-id.schema.json";
328 pub const FLOW_ID: &str =
330 "https://greentic-ai.github.io/greentic-types/schemas/v1/flow-id.schema.json";
331 pub const NODE_ID: &str =
333 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-id.schema.json";
334 pub const TENANT_CONTEXT: &str =
336 "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-context.schema.json";
337 pub const HASH_DIGEST: &str =
339 "https://greentic-ai.github.io/greentic-types/schemas/v1/hash-digest.schema.json";
340 pub const SEMVER_REQ: &str =
342 "https://greentic-ai.github.io/greentic-types/schemas/v1/semver-req.schema.json";
343 pub const REDACTION_PATH: &str =
345 "https://greentic-ai.github.io/greentic-types/schemas/v1/redaction-path.schema.json";
346 pub const CAPABILITIES: &str =
348 "https://greentic-ai.github.io/greentic-types/schemas/v1/capabilities.schema.json";
349 pub const REPO_SKIN: &str =
351 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-skin.schema.json";
352 pub const REPO_AUTH: &str =
354 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-auth.schema.json";
355 pub const REPO_TENANT_CONFIG: &str =
357 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-tenant-config.schema.json";
358 pub const TENANT_DID_DOCUMENT: &str =
360 "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-did-document.schema.json";
361 pub const FLOW: &str = "greentic.flow.v1";
363 pub const FLOW_RESOLVE: &str = "greentic.flow.resolve.v1";
365 pub const FLOW_RESOLVE_SUMMARY: &str = "greentic.flow.resolve-summary.v1";
367 pub const NODE: &str =
369 "https://greentic-ai.github.io/greentic-types/schemas/v1/node.schema.json";
370 pub const COMPONENT_MANIFEST: &str =
372 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-manifest.schema.json";
373 pub const PACK_MANIFEST: &str = "greentic.pack-manifest.v1";
375 pub const VALIDATION_SEVERITY: &str =
377 "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-severity.schema.json";
378 pub const VALIDATION_DIAGNOSTIC: &str =
380 "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-diagnostic.schema.json";
381 pub const VALIDATION_REPORT: &str =
383 "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-report.schema.json";
384 pub const PROVIDER_MANIFEST: &str =
386 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-manifest.schema.json";
387 pub const PROVIDER_RUNTIME_REF: &str =
389 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-runtime-ref.schema.json";
390 pub const PROVIDER_DECL: &str =
392 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-decl.schema.json";
393 pub const PROVIDER_EXTENSION_INLINE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-extension-inline.schema.json";
395 pub const PROVIDER_INSTALL_RECORD: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-install-record.schema.json";
397 pub const LIMITS: &str =
399 "https://greentic-ai.github.io/greentic-types/schemas/v1/limits.schema.json";
400 pub const TELEMETRY_SPEC: &str =
402 "https://greentic-ai.github.io/greentic-types/schemas/v1/telemetry-spec.schema.json";
403 pub const NODE_SUMMARY: &str =
405 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-summary.schema.json";
406 pub const NODE_FAILURE: &str =
408 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-failure.schema.json";
409 pub const NODE_STATUS: &str =
411 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-status.schema.json";
412 pub const RUN_STATUS: &str =
414 "https://greentic-ai.github.io/greentic-types/schemas/v1/run-status.schema.json";
415 pub const TRANSCRIPT_OFFSET: &str =
417 "https://greentic-ai.github.io/greentic-types/schemas/v1/transcript-offset.schema.json";
418 pub const TOOLS_CAPS: &str =
420 "https://greentic-ai.github.io/greentic-types/schemas/v1/tools-caps.schema.json";
421 pub const SECRETS_CAPS: &str =
423 "https://greentic-ai.github.io/greentic-types/schemas/v1/secrets-caps.schema.json";
424 pub const BRANCH_REF: &str =
426 "https://greentic-ai.github.io/greentic-types/schemas/v1/branch-ref.schema.json";
427 pub const COMMIT_REF: &str =
429 "https://greentic-ai.github.io/greentic-types/schemas/v1/commit-ref.schema.json";
430 pub const GIT_PROVIDER_REF: &str =
432 "https://greentic-ai.github.io/greentic-types/schemas/v1/git-provider-ref.schema.json";
433 pub const SCANNER_REF: &str =
435 "https://greentic-ai.github.io/greentic-types/schemas/v1/scanner-ref.schema.json";
436 pub const WEBHOOK_ID: &str =
438 "https://greentic-ai.github.io/greentic-types/schemas/v1/webhook-id.schema.json";
439 pub const PROVIDER_INSTALL_ID: &str =
441 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-install-id.schema.json";
442 pub const REPO_REF: &str =
444 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-ref.schema.json";
445 pub const COMPONENT_REF: &str =
447 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-ref.schema.json";
448 pub const VERSION_REF: &str =
450 "https://greentic-ai.github.io/greentic-types/schemas/v1/version-ref.schema.json";
451 pub const BUILD_REF: &str =
453 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-ref.schema.json";
454 pub const SCAN_REF: &str =
456 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-ref.schema.json";
457 pub const ATTESTATION_REF: &str =
459 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-ref.schema.json";
460 pub const ATTESTATION_ID: &str =
462 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-id.schema.json";
463 pub const POLICY_REF: &str =
465 "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-ref.schema.json";
466 pub const POLICY_INPUT_REF: &str =
468 "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-input-ref.schema.json";
469 pub const STORE_REF: &str =
471 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-ref.schema.json";
472 pub const REGISTRY_REF: &str =
474 "https://greentic-ai.github.io/greentic-types/schemas/v1/registry-ref.schema.json";
475 pub const OCI_IMAGE_REF: &str =
477 "https://greentic-ai.github.io/greentic-types/schemas/v1/oci-image-ref.schema.json";
478 pub const ARTIFACT_REF: &str =
480 "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-ref.schema.json";
481 pub const SBOM_REF: &str =
483 "https://greentic-ai.github.io/greentic-types/schemas/v1/sbom-ref.schema.json";
484 pub const SIGNING_KEY_REF: &str =
486 "https://greentic-ai.github.io/greentic-types/schemas/v1/signing-key-ref.schema.json";
487 pub const SIGNATURE_REF: &str =
489 "https://greentic-ai.github.io/greentic-types/schemas/v1/signature-ref.schema.json";
490 pub const STATEMENT_REF: &str =
492 "https://greentic-ai.github.io/greentic-types/schemas/v1/statement-ref.schema.json";
493 pub const BUILD_LOG_REF: &str =
495 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-log-ref.schema.json";
496 pub const METADATA_RECORD_REF: &str =
498 "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record-ref.schema.json";
499 pub const API_KEY_REF: &str =
501 "https://greentic-ai.github.io/greentic-types/schemas/v1/api-key-ref.schema.json";
502 pub const ENVIRONMENT_REF: &str =
504 "https://greentic-ai.github.io/greentic-types/schemas/v1/environment-ref.schema.json";
505 pub const DISTRIBUTOR_REF: &str =
507 "https://greentic-ai.github.io/greentic-types/schemas/v1/distributor-ref.schema.json";
508 pub const STOREFRONT_ID: &str =
510 "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront-id.schema.json";
511 pub const STORE_PRODUCT_ID: &str =
513 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-id.schema.json";
514 pub const STORE_PLAN_ID: &str =
516 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan-id.schema.json";
517 pub const SUBSCRIPTION_ID: &str =
519 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-id.schema.json";
520 pub const BUNDLE_ID: &str =
522 "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle-id.schema.json";
523 pub const COLLECTION_ID: &str =
525 "https://greentic-ai.github.io/greentic-types/schemas/v1/collection-id.schema.json";
526 pub const ARTIFACT_SELECTOR: &str =
528 "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-selector.schema.json";
529 pub const CAPABILITY_MAP: &str =
531 "https://greentic-ai.github.io/greentic-types/schemas/v1/capability-map.schema.json";
532 pub const STORE_PRODUCT_KIND: &str =
534 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-kind.schema.json";
535 pub const VERSION_STRATEGY: &str =
537 "https://greentic-ai.github.io/greentic-types/schemas/v1/version-strategy.schema.json";
538 pub const ROLLOUT_STATUS: &str =
540 "https://greentic-ai.github.io/greentic-types/schemas/v1/rollout-status.schema.json";
541 pub const CONNECTION_KIND: &str =
543 "https://greentic-ai.github.io/greentic-types/schemas/v1/connection-kind.schema.json";
544 pub const PACK_OR_COMPONENT_REF: &str =
546 "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-or-component-ref.schema.json";
547 pub const PLAN_LIMITS: &str =
549 "https://greentic-ai.github.io/greentic-types/schemas/v1/plan-limits.schema.json";
550 pub const PRICE_MODEL: &str =
552 "https://greentic-ai.github.io/greentic-types/schemas/v1/price-model.schema.json";
553 pub const SUBSCRIPTION_STATUS: &str =
555 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-status.schema.json";
556 pub const BUILD_PLAN: &str =
558 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-plan.schema.json";
559 pub const BUILD_STATUS: &str =
561 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-status.schema.json";
562 pub const SCAN_REQUEST: &str =
564 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-request.schema.json";
565 pub const SCAN_RESULT: &str =
567 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-result.schema.json";
568 pub const SIGN_REQUEST: &str =
570 "https://greentic-ai.github.io/greentic-types/schemas/v1/sign-request.schema.json";
571 pub const VERIFY_REQUEST: &str =
573 "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-request.schema.json";
574 pub const VERIFY_RESULT: &str =
576 "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-result.schema.json";
577 pub const ATTESTATION_STATEMENT: &str =
579 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-statement.schema.json";
580 pub const METADATA_RECORD: &str =
582 "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record.schema.json";
583 pub const REPO_CONTEXT: &str =
585 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-context.schema.json";
586 pub const STORE_CONTEXT: &str =
588 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-context.schema.json";
589 pub const BUNDLE: &str =
591 "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle.schema.json";
592 pub const DESIRED_STATE_EXPORT: &str =
594 "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state-export.schema.json";
595 pub const DESIRED_STATE: &str =
597 "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state.schema.json";
598 pub const DESIRED_SUBSCRIPTION_ENTRY: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-subscription-entry.schema.json";
600 pub const STOREFRONT: &str =
602 "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront.schema.json";
603 pub const STORE_PRODUCT: &str =
605 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product.schema.json";
606 pub const STORE_PLAN: &str =
608 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan.schema.json";
609 pub const SUBSCRIPTION: &str =
611 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription.schema.json";
612 pub const ENVIRONMENT: &str =
614 "https://greentic-ai.github.io/greentic-types/schemas/v1/environment.schema.json";
615 pub const THEME: &str =
617 "https://greentic-ai.github.io/greentic-types/schemas/v1/theme.schema.json";
618 pub const LAYOUT_SECTION: &str =
620 "https://greentic-ai.github.io/greentic-types/schemas/v1/layout-section.schema.json";
621 pub const COLLECTION: &str =
623 "https://greentic-ai.github.io/greentic-types/schemas/v1/collection.schema.json";
624 pub const PRODUCT_OVERRIDE: &str =
626 "https://greentic-ai.github.io/greentic-types/schemas/v1/product-override.schema.json";
627 pub const EVENT_ENVELOPE: &str =
629 "https://greentic-ai.github.io/greentic-types/schemas/v1/event-envelope.schema.json";
630 pub const EVENT_PROVIDER_DESCRIPTOR: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/event-provider-descriptor.schema.json";
632 pub const CHANNEL_MESSAGE_ENVELOPE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/channel-message-envelope.schema.json";
634 pub const ATTACHMENT: &str =
636 "https://greentic-ai.github.io/greentic-types/schemas/v1/attachment.schema.json";
637 pub const WORKER_REQUEST: &str =
639 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-request.schema.json";
640 pub const WORKER_MESSAGE: &str =
642 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-message.schema.json";
643 pub const WORKER_RESPONSE: &str =
645 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-response.schema.json";
646 pub const OTLP_KEYS: &str =
648 "https://greentic-ai.github.io/greentic-types/schemas/v1/otlp-keys.schema.json";
649 pub const RUN_RESULT: &str =
651 "https://greentic-ai.github.io/greentic-types/schemas/v1/run-result.schema.json";
652}
653
654#[cfg(all(feature = "schema", feature = "std"))]
655pub fn write_all_schemas(out_dir: &std::path::Path) -> anyhow::Result<()> {
657 use anyhow::Context;
658 use std::fs;
659
660 fs::create_dir_all(out_dir)
661 .with_context(|| format!("failed to create {}", out_dir.display()))?;
662
663 for entry in crate::schema::entries() {
664 let schema = (entry.generator)();
665 let path = out_dir.join(entry.file_name);
666 if let Some(parent) = path.parent() {
667 fs::create_dir_all(parent)
668 .with_context(|| format!("failed to create {}", parent.display()))?;
669 }
670
671 let json =
672 serde_json::to_vec_pretty(&schema).context("failed to serialize schema to JSON")?;
673 fs::write(&path, json).with_context(|| format!("failed to write {}", path.display()))?;
674 }
675
676 Ok(())
677}
678
679macro_rules! id_newtype {
680 ($name:ident, $doc:literal) => {
681 #[doc = $doc]
682 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
683 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
684 #[cfg_attr(feature = "schemars", derive(JsonSchema))]
685 #[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
686 pub struct $name(pub String);
687
688 impl $name {
689 pub fn as_str(&self) -> &str {
691 &self.0
692 }
693
694 pub fn new(value: impl AsRef<str>) -> GResult<Self> {
696 value.as_ref().parse()
697 }
698 }
699
700 impl From<$name> for String {
701 fn from(value: $name) -> Self {
702 value.0
703 }
704 }
705
706 impl AsRef<str> for $name {
707 fn as_ref(&self) -> &str {
708 self.as_str()
709 }
710 }
711
712 impl fmt::Display for $name {
713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714 f.write_str(self.as_str())
715 }
716 }
717
718 impl FromStr for $name {
719 type Err = GreenticError;
720
721 fn from_str(value: &str) -> Result<Self, Self::Err> {
722 validate_identifier(value, stringify!($name))?;
723 Ok(Self(value.to_owned()))
724 }
725 }
726
727 impl TryFrom<String> for $name {
728 type Error = GreenticError;
729
730 fn try_from(value: String) -> Result<Self, Self::Error> {
731 $name::from_str(&value)
732 }
733 }
734
735 impl TryFrom<&str> for $name {
736 type Error = GreenticError;
737
738 fn try_from(value: &str) -> Result<Self, Self::Error> {
739 $name::from_str(value)
740 }
741 }
742 };
743}
744
745id_newtype!(EnvId, "Environment identifier for a tenant context.");
746id_newtype!(TenantId, "Tenant identifier within an environment.");
747id_newtype!(TeamId, "Team identifier belonging to a tenant.");
748id_newtype!(UserId, "User identifier within a tenant.");
749id_newtype!(BranchRef, "Reference to a source control branch.");
750id_newtype!(CommitRef, "Reference to a source control commit.");
751id_newtype!(
752 GitProviderRef,
753 "Identifier referencing a source control provider."
754);
755id_newtype!(ScannerRef, "Identifier referencing a scanner provider.");
756id_newtype!(WebhookId, "Identifier referencing a registered webhook.");
757id_newtype!(
758 ProviderInstallId,
759 "Identifier referencing a provider installation record."
760);
761id_newtype!(PackId, "Globally unique pack identifier.");
762id_newtype!(
763 ComponentId,
764 "Identifier referencing a component binding in a pack."
765);
766id_newtype!(FlowId, "Identifier referencing a flow inside a pack.");
767id_newtype!(NodeId, "Identifier referencing a node inside a flow graph.");
768id_newtype!(
769 EnvironmentRef,
770 "Identifier referencing a deployment environment."
771);
772id_newtype!(
773 DistributorRef,
774 "Identifier referencing a distributor instance."
775);
776id_newtype!(StoreFrontId, "Identifier referencing a storefront.");
777id_newtype!(
778 StoreProductId,
779 "Identifier referencing a product in the store catalog."
780);
781id_newtype!(
782 StorePlanId,
783 "Identifier referencing a plan for a store product."
784);
785id_newtype!(
786 SubscriptionId,
787 "Identifier referencing a subscription entry."
788);
789id_newtype!(BundleId, "Identifier referencing a distributor bundle.");
790id_newtype!(CollectionId, "Identifier referencing a product collection.");
791id_newtype!(RepoRef, "Repository reference within a supply chain.");
792id_newtype!(
793 ComponentRef,
794 "Supply-chain component reference (distinct from pack ComponentId)."
795);
796id_newtype!(
797 VersionRef,
798 "Version reference for a component or metadata record."
799);
800id_newtype!(BuildRef, "Build reference within a supply chain.");
801id_newtype!(ScanRef, "Scan reference within a supply chain.");
802id_newtype!(
803 AttestationRef,
804 "Attestation reference within a supply chain."
805);
806id_newtype!(AttestationId, "Identifier referencing an attestation.");
807id_newtype!(PolicyRef, "Policy reference within a supply chain.");
808id_newtype!(
809 PolicyInputRef,
810 "Reference to a policy input payload for evaluation."
811);
812id_newtype!(StoreRef, "Content store reference within a supply chain.");
813id_newtype!(
814 RegistryRef,
815 "Registry reference for OCI or artifact storage."
816);
817id_newtype!(
818 OciImageRef,
819 "Reference to an OCI image for distribution (oci://repo/name:tag or oci://repo/name@sha256:...)."
820);
821id_newtype!(
822 ArtifactRef,
823 "Artifact reference within a build or scan result."
824);
825id_newtype!(
826 SbomRef,
827 "Reference to a Software Bill of Materials artifact."
828);
829id_newtype!(SigningKeyRef, "Reference to a signing key handle.");
830id_newtype!(SignatureRef, "Reference to a generated signature.");
831id_newtype!(StatementRef, "Reference to an attestation statement.");
832id_newtype!(
833 BuildLogRef,
834 "Reference to a build log output produced during execution."
835);
836id_newtype!(
837 MetadataRecordRef,
838 "Reference to a metadata record attached to artifacts or bundles."
839);
840
841#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
843#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
844#[cfg_attr(feature = "schemars", derive(JsonSchema))]
845#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
846pub struct ApiKeyRef(pub String);
847
848impl ApiKeyRef {
849 pub fn as_str(&self) -> &str {
851 &self.0
852 }
853
854 pub fn new(value: impl AsRef<str>) -> GResult<Self> {
856 value.as_ref().parse()
857 }
858}
859
860impl fmt::Display for ApiKeyRef {
861 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
862 f.write_str(self.as_str())
863 }
864}
865
866impl FromStr for ApiKeyRef {
867 type Err = GreenticError;
868
869 fn from_str(value: &str) -> Result<Self, Self::Err> {
870 validate_api_key_ref(value)?;
871 Ok(Self(value.to_owned()))
872 }
873}
874
875impl TryFrom<String> for ApiKeyRef {
876 type Error = GreenticError;
877
878 fn try_from(value: String) -> Result<Self, Self::Error> {
879 ApiKeyRef::from_str(&value)
880 }
881}
882
883impl TryFrom<&str> for ApiKeyRef {
884 type Error = GreenticError;
885
886 fn try_from(value: &str) -> Result<Self, Self::Error> {
887 ApiKeyRef::from_str(value)
888 }
889}
890
891impl From<ApiKeyRef> for String {
892 fn from(value: ApiKeyRef) -> Self {
893 value.0
894 }
895}
896
897impl AsRef<str> for ApiKeyRef {
898 fn as_ref(&self) -> &str {
899 self.as_str()
900 }
901}
902
903#[derive(Clone, Debug, PartialEq, Eq, Hash)]
905#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
906#[cfg_attr(feature = "schemars", derive(JsonSchema))]
907pub struct TenantContext {
908 pub tenant_id: TenantId,
910 #[cfg_attr(
912 feature = "serde",
913 serde(default, skip_serializing_if = "Option::is_none")
914 )]
915 pub team_id: Option<TeamId>,
916 #[cfg_attr(
918 feature = "serde",
919 serde(default, skip_serializing_if = "Option::is_none")
920 )]
921 pub user_id: Option<UserId>,
922 #[cfg_attr(
924 feature = "serde",
925 serde(default, skip_serializing_if = "Option::is_none")
926 )]
927 pub session_id: Option<String>,
928 #[cfg_attr(
930 feature = "serde",
931 serde(default, skip_serializing_if = "BTreeMap::is_empty")
932 )]
933 pub attributes: BTreeMap<String, String>,
934}
935
936impl TenantContext {
937 pub fn new(tenant_id: TenantId) -> Self {
939 Self {
940 tenant_id,
941 team_id: None,
942 user_id: None,
943 session_id: None,
944 attributes: BTreeMap::new(),
945 }
946 }
947}
948
949impl From<&TenantCtx> for TenantContext {
950 fn from(ctx: &TenantCtx) -> Self {
951 Self {
952 tenant_id: ctx.tenant_id.clone(),
953 team_id: ctx.team_id.clone().or_else(|| ctx.team.clone()),
954 user_id: ctx.user_id.clone().or_else(|| ctx.user.clone()),
955 session_id: ctx.session_id.clone(),
956 attributes: ctx.attributes.clone(),
957 }
958 }
959}
960
961#[derive(Clone, Debug, PartialEq, Eq, Hash)]
963#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
964#[cfg_attr(feature = "schemars", derive(JsonSchema))]
965#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
966pub enum HashAlgorithm {
967 Blake3,
969 Other(String),
971}
972
973#[derive(Clone, Debug, PartialEq, Eq, Hash)]
975#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
976#[cfg_attr(
977 feature = "serde",
978 serde(into = "HashDigestRepr", try_from = "HashDigestRepr")
979)]
980#[cfg_attr(feature = "schemars", derive(JsonSchema))]
981pub struct HashDigest {
982 pub algo: HashAlgorithm,
984 pub hex: String,
986}
987
988impl HashDigest {
989 pub fn new(algo: HashAlgorithm, hex: impl Into<String>) -> GResult<Self> {
991 let hex = hex.into();
992 validate_hex(&hex)?;
993 Ok(Self { algo, hex })
994 }
995
996 pub fn blake3(hex: impl Into<String>) -> GResult<Self> {
998 Self::new(HashAlgorithm::Blake3, hex)
999 }
1000}
1001
1002#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1003#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1004struct HashDigestRepr {
1005 algo: HashAlgorithm,
1006 hex: String,
1007}
1008
1009impl From<HashDigest> for HashDigestRepr {
1010 fn from(value: HashDigest) -> Self {
1011 Self {
1012 algo: value.algo,
1013 hex: value.hex,
1014 }
1015 }
1016}
1017
1018impl TryFrom<HashDigestRepr> for HashDigest {
1019 type Error = GreenticError;
1020
1021 fn try_from(value: HashDigestRepr) -> Result<Self, Self::Error> {
1022 HashDigest::new(value.algo, value.hex)
1023 }
1024}
1025
1026fn validate_hex(hex: &str) -> GResult<()> {
1027 if hex.is_empty() {
1028 return Err(GreenticError::new(
1029 ErrorCode::InvalidInput,
1030 "digest hex payload must not be empty",
1031 ));
1032 }
1033 if hex.len() % 2 != 0 {
1034 return Err(GreenticError::new(
1035 ErrorCode::InvalidInput,
1036 "digest hex payload must have an even number of digits",
1037 ));
1038 }
1039 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
1040 return Err(GreenticError::new(
1041 ErrorCode::InvalidInput,
1042 "digest hex payload must be hexadecimal",
1043 ));
1044 }
1045 Ok(())
1046}
1047
1048#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1050#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1051#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1052pub struct SemverReq(String);
1053
1054impl SemverReq {
1055 pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1057 let value = value.as_ref();
1058 VersionReq::parse(value).map_err(|err| {
1059 GreenticError::new(
1060 ErrorCode::InvalidInput,
1061 format!("invalid semver requirement '{value}': {err}"),
1062 )
1063 })?;
1064 Ok(Self(value.to_owned()))
1065 }
1066
1067 pub fn as_str(&self) -> &str {
1069 &self.0
1070 }
1071
1072 pub fn to_version_req(&self) -> VersionReq {
1074 VersionReq::parse(&self.0)
1075 .unwrap_or_else(|err| unreachable!("SemverReq::parse validated inputs: {err}"))
1076 }
1077}
1078
1079impl fmt::Display for SemverReq {
1080 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1081 f.write_str(self.as_str())
1082 }
1083}
1084
1085impl From<SemverReq> for String {
1086 fn from(value: SemverReq) -> Self {
1087 value.0
1088 }
1089}
1090
1091impl TryFrom<String> for SemverReq {
1092 type Error = GreenticError;
1093
1094 fn try_from(value: String) -> Result<Self, Self::Error> {
1095 SemverReq::parse(&value)
1096 }
1097}
1098
1099impl TryFrom<&str> for SemverReq {
1100 type Error = GreenticError;
1101
1102 fn try_from(value: &str) -> Result<Self, Self::Error> {
1103 SemverReq::parse(value)
1104 }
1105}
1106
1107impl FromStr for SemverReq {
1108 type Err = GreenticError;
1109
1110 fn from_str(s: &str) -> Result<Self, Self::Err> {
1111 SemverReq::parse(s)
1112 }
1113}
1114
1115#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1117#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1118#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1119pub struct RedactionPath(String);
1120
1121impl RedactionPath {
1122 pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1124 let value = value.as_ref();
1125 validate_jsonpath(value)?;
1126 Ok(Self(value.to_owned()))
1127 }
1128
1129 pub fn as_str(&self) -> &str {
1131 &self.0
1132 }
1133}
1134
1135impl fmt::Display for RedactionPath {
1136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1137 f.write_str(self.as_str())
1138 }
1139}
1140
1141impl From<RedactionPath> for String {
1142 fn from(value: RedactionPath) -> Self {
1143 value.0
1144 }
1145}
1146
1147impl TryFrom<String> for RedactionPath {
1148 type Error = GreenticError;
1149
1150 fn try_from(value: String) -> Result<Self, Self::Error> {
1151 RedactionPath::parse(&value)
1152 }
1153}
1154
1155impl TryFrom<&str> for RedactionPath {
1156 type Error = GreenticError;
1157
1158 fn try_from(value: &str) -> Result<Self, Self::Error> {
1159 RedactionPath::parse(value)
1160 }
1161}
1162
1163fn validate_jsonpath(path: &str) -> GResult<()> {
1164 if path.is_empty() {
1165 return Err(GreenticError::new(
1166 ErrorCode::InvalidInput,
1167 "redaction path cannot be empty",
1168 ));
1169 }
1170 if !path.starts_with('$') {
1171 return Err(GreenticError::new(
1172 ErrorCode::InvalidInput,
1173 "redaction path must start with '$'",
1174 ));
1175 }
1176 if path.chars().any(|c| c.is_control()) {
1177 return Err(GreenticError::new(
1178 ErrorCode::InvalidInput,
1179 "redaction path cannot contain control characters",
1180 ));
1181 }
1182 Ok(())
1183}
1184
1185#[cfg(feature = "schemars")]
1186impl JsonSchema for SemverReq {
1187 fn schema_name() -> Cow<'static, str> {
1188 Cow::Borrowed("SemverReq")
1189 }
1190
1191 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1192 let mut schema = <String>::json_schema(generator);
1193 if schema.get("description").is_none() {
1194 schema.insert(
1195 "description".into(),
1196 "Validated semantic version requirement string".into(),
1197 );
1198 }
1199 schema
1200 }
1201}
1202
1203#[cfg(feature = "schemars")]
1204impl JsonSchema for RedactionPath {
1205 fn schema_name() -> Cow<'static, str> {
1206 Cow::Borrowed("RedactionPath")
1207 }
1208
1209 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1210 let mut schema = <String>::json_schema(generator);
1211 if schema.get("description").is_none() {
1212 schema.insert(
1213 "description".into(),
1214 "JSONPath expression used for runtime redaction".into(),
1215 );
1216 }
1217 schema
1218 }
1219}
1220
1221#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1223#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1224#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1225pub struct InvocationDeadline {
1226 unix_millis: i128,
1227}
1228
1229impl InvocationDeadline {
1230 pub const fn from_unix_millis(unix_millis: i128) -> Self {
1232 Self { unix_millis }
1233 }
1234
1235 pub const fn unix_millis(&self) -> i128 {
1237 self.unix_millis
1238 }
1239
1240 #[cfg(feature = "time")]
1242 pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, time::error::ComponentRange> {
1243 OffsetDateTime::from_unix_timestamp_nanos(self.unix_millis * 1_000_000)
1244 }
1245
1246 #[cfg(feature = "time")]
1248 pub fn from_offset_date_time(value: OffsetDateTime) -> Self {
1249 let nanos = value.unix_timestamp_nanos();
1250 Self {
1251 unix_millis: nanos / 1_000_000,
1252 }
1253 }
1254}
1255
1256#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1258#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1259#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1260pub struct TenantCtx {
1261 pub env: EnvId,
1263 pub tenant: TenantId,
1265 pub tenant_id: TenantId,
1267 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1269 pub team: Option<TeamId>,
1270 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1272 pub team_id: Option<TeamId>,
1273 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1275 pub user: Option<UserId>,
1276 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1278 pub user_id: Option<UserId>,
1279 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1281 pub session_id: Option<String>,
1282 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1284 pub flow_id: Option<String>,
1285 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1287 pub node_id: Option<String>,
1288 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1290 pub provider_id: Option<String>,
1291 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1293 pub trace_id: Option<String>,
1294 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1296 pub i18n_id: Option<String>,
1297 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1299 pub correlation_id: Option<String>,
1300 #[cfg_attr(
1302 feature = "serde",
1303 serde(default, skip_serializing_if = "BTreeMap::is_empty")
1304 )]
1305 pub attributes: BTreeMap<String, String>,
1306 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1308 pub deadline: Option<InvocationDeadline>,
1309 pub attempt: u32,
1311 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1313 pub idempotency_key: Option<String>,
1314 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1316 pub impersonation: Option<Impersonation>,
1317}
1318
1319impl TenantCtx {
1320 pub fn new(env: EnvId, tenant: TenantId) -> Self {
1322 let tenant_id = tenant.clone();
1323 Self {
1324 env,
1325 tenant: tenant.clone(),
1326 tenant_id,
1327 team: None,
1328 team_id: None,
1329 user: None,
1330 user_id: None,
1331 session_id: None,
1332 flow_id: None,
1333 node_id: None,
1334 provider_id: None,
1335 trace_id: None,
1336 i18n_id: None,
1337 correlation_id: None,
1338 attributes: BTreeMap::new(),
1339 deadline: None,
1340 attempt: 0,
1341 idempotency_key: None,
1342 impersonation: None,
1343 }
1344 }
1345
1346 pub fn with_team(mut self, team: Option<TeamId>) -> Self {
1348 self.team = team.clone();
1349 self.team_id = team;
1350 self
1351 }
1352
1353 pub fn with_user(mut self, user: Option<UserId>) -> Self {
1355 self.user = user.clone();
1356 self.user_id = user;
1357 self
1358 }
1359
1360 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1362 self.session_id = Some(session.into());
1363 self
1364 }
1365
1366 pub fn with_flow(mut self, flow: impl Into<String>) -> Self {
1368 self.flow_id = Some(flow.into());
1369 self
1370 }
1371
1372 pub fn with_node(mut self, node: impl Into<String>) -> Self {
1374 self.node_id = Some(node.into());
1375 self
1376 }
1377
1378 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1380 self.provider_id = Some(provider.into());
1381 self
1382 }
1383
1384 pub fn with_attributes(mut self, attributes: BTreeMap<String, String>) -> Self {
1386 self.attributes = attributes;
1387 self
1388 }
1389
1390 pub fn with_impersonation(mut self, impersonation: Option<Impersonation>) -> Self {
1392 self.impersonation = impersonation;
1393 self
1394 }
1395
1396 pub fn with_attempt(mut self, attempt: u32) -> Self {
1398 self.attempt = attempt;
1399 self
1400 }
1401
1402 pub fn with_deadline(mut self, deadline: Option<InvocationDeadline>) -> Self {
1404 self.deadline = deadline;
1405 self
1406 }
1407
1408 pub fn session_id(&self) -> Option<&str> {
1410 self.session_id.as_deref()
1411 }
1412
1413 pub fn flow_id(&self) -> Option<&str> {
1415 self.flow_id.as_deref()
1416 }
1417
1418 pub fn node_id(&self) -> Option<&str> {
1420 self.node_id.as_deref()
1421 }
1422
1423 pub fn provider_id(&self) -> Option<&str> {
1425 self.provider_id.as_deref()
1426 }
1427}
1428
1429pub type BinaryPayload = Vec<u8>;
1431
1432#[derive(Clone, Debug, PartialEq, Eq)]
1434#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1435#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1436pub struct InvocationEnvelope {
1437 pub ctx: TenantCtx,
1439 pub flow_id: String,
1441 pub node_id: Option<String>,
1443 pub op: String,
1445 pub payload: BinaryPayload,
1447 pub metadata: BinaryPayload,
1449}
1450
1451#[derive(Clone, Debug, PartialEq, Eq)]
1453#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1454#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1455pub enum ErrorDetail {
1456 Text(String),
1458 Binary(BinaryPayload),
1460}
1461
1462#[derive(Debug)]
1464#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1465#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1466pub struct NodeError {
1467 pub code: String,
1469 pub message: String,
1471 pub retryable: bool,
1473 pub backoff_ms: Option<u64>,
1475 pub details: Option<ErrorDetail>,
1477 #[cfg(feature = "std")]
1478 #[cfg_attr(feature = "serde", serde(skip, default = "default_source"))]
1479 #[cfg_attr(feature = "schemars", schemars(skip))]
1480 source: Option<Box<dyn StdError + Send + Sync>>,
1481}
1482
1483#[cfg(all(feature = "std", feature = "serde"))]
1484fn default_source() -> Option<Box<dyn StdError + Send + Sync>> {
1485 None
1486}
1487
1488impl NodeError {
1489 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
1491 Self {
1492 code: code.into(),
1493 message: message.into(),
1494 retryable: false,
1495 backoff_ms: None,
1496 details: None,
1497 #[cfg(feature = "std")]
1498 source: None,
1499 }
1500 }
1501
1502 pub fn with_retry(mut self, backoff_ms: Option<u64>) -> Self {
1504 self.retryable = true;
1505 self.backoff_ms = backoff_ms;
1506 self
1507 }
1508
1509 pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
1511 self.details = Some(detail);
1512 self
1513 }
1514
1515 pub fn with_detail_text(mut self, detail: impl Into<String>) -> Self {
1517 self.details = Some(ErrorDetail::Text(detail.into()));
1518 self
1519 }
1520
1521 pub fn with_detail_binary(mut self, detail: BinaryPayload) -> Self {
1523 self.details = Some(ErrorDetail::Binary(detail));
1524 self
1525 }
1526
1527 #[cfg(feature = "std")]
1528 pub fn with_source<E>(mut self, source: E) -> Self
1530 where
1531 E: StdError + Send + Sync + 'static,
1532 {
1533 self.source = Some(Box::new(source));
1534 self
1535 }
1536
1537 pub fn detail(&self) -> Option<&ErrorDetail> {
1539 self.details.as_ref()
1540 }
1541
1542 #[cfg(feature = "std")]
1543 pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
1545 self.source.as_deref()
1546 }
1547}
1548
1549impl fmt::Display for NodeError {
1550 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1551 write!(f, "{}: {}", self.code, self.message)
1552 }
1553}
1554
1555#[cfg(feature = "std")]
1556impl StdError for NodeError {
1557 fn source(&self) -> Option<&(dyn StdError + 'static)> {
1558 self.source
1559 .as_ref()
1560 .map(|err| err.as_ref() as &(dyn StdError + 'static))
1561 }
1562}
1563
1564pub type NodeResult<T> = Result<T, NodeError>;
1566
1567pub fn make_idempotency_key(
1572 ctx: &TenantCtx,
1573 flow_id: &str,
1574 node_id: Option<&str>,
1575 correlation: Option<&str>,
1576) -> String {
1577 let node_segment = node_id.unwrap_or_default();
1578 let correlation_segment = correlation
1579 .or(ctx.correlation_id.as_deref())
1580 .unwrap_or_default();
1581 let input = format!(
1582 "{}|{}|{}|{}",
1583 ctx.tenant_id.as_str(),
1584 flow_id,
1585 node_segment,
1586 correlation_segment
1587 );
1588 fnv1a_128_hex(input.as_bytes())
1589}
1590
1591const FNV_OFFSET_BASIS: u128 = 0x6c62272e07bb014262b821756295c58d;
1592const FNV_PRIME: u128 = 0x0000000001000000000000000000013b;
1593
1594fn fnv1a_128_hex(bytes: &[u8]) -> String {
1595 let mut hash = FNV_OFFSET_BASIS;
1596 for &byte in bytes {
1597 hash ^= byte as u128;
1598 hash = hash.wrapping_mul(FNV_PRIME);
1599 }
1600
1601 let mut output = String::with_capacity(32);
1602 for shift in (0..32).rev() {
1603 let nibble = ((hash >> (shift * 4)) & 0x0f) as u8;
1604 output.push(match nibble {
1605 0..=9 => (b'0' + nibble) as char,
1606 _ => (b'a' + (nibble - 10)) as char,
1607 });
1608 }
1609 output
1610}
1611
1612#[cfg(test)]
1613mod tests {
1614 use super::*;
1615 use core::convert::TryFrom;
1616 use time::OffsetDateTime;
1617
1618 fn sample_ctx() -> TenantCtx {
1619 let env = EnvId::try_from("prod").unwrap_or_else(|err| panic!("{err}"));
1620 let tenant = TenantId::try_from("tenant-123").unwrap_or_else(|err| panic!("{err}"));
1621 let team = TeamId::try_from("team-456").unwrap_or_else(|err| panic!("{err}"));
1622 let user = UserId::try_from("user-789").unwrap_or_else(|err| panic!("{err}"));
1623
1624 let mut ctx = TenantCtx::new(env, tenant)
1625 .with_team(Some(team))
1626 .with_user(Some(user))
1627 .with_attempt(2)
1628 .with_deadline(Some(InvocationDeadline::from_unix_millis(
1629 1_700_000_000_000,
1630 )));
1631 ctx.trace_id = Some("trace-abc".to_owned());
1632 ctx.correlation_id = Some("corr-xyz".to_owned());
1633 ctx.idempotency_key = Some("key-123".to_owned());
1634 ctx
1635 }
1636
1637 #[test]
1638 fn idempotent_key_stable() {
1639 let ctx = sample_ctx();
1640 let key_a = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1641 let key_b = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1642 assert_eq!(key_a, key_b);
1643 assert_eq!(key_a.len(), 32);
1644 }
1645
1646 #[test]
1647 fn idempotent_key_uses_context_correlation() {
1648 let ctx = sample_ctx();
1649 let key = make_idempotency_key(&ctx, "flow-1", None, None);
1650 let expected = make_idempotency_key(&ctx, "flow-1", None, ctx.correlation_id.as_deref());
1651 assert_eq!(key, expected);
1652 }
1653
1654 #[test]
1655 #[cfg(feature = "time")]
1656 fn deadline_roundtrips_through_offset_datetime() {
1657 let dt = OffsetDateTime::from_unix_timestamp(1_700_000_000)
1658 .unwrap_or_else(|err| panic!("valid timestamp: {err}"));
1659 let deadline = InvocationDeadline::from_offset_date_time(dt);
1660 let roundtrip = deadline
1661 .to_offset_date_time()
1662 .unwrap_or_else(|err| panic!("round-trip conversion failed: {err}"));
1663 let millis = dt.unix_timestamp_nanos() / 1_000_000;
1664 assert_eq!(deadline.unix_millis(), millis);
1665 assert_eq!(roundtrip.unix_timestamp_nanos() / 1_000_000, millis);
1666 }
1667
1668 #[test]
1669 fn node_error_builder_sets_fields() {
1670 let err = NodeError::new("TEST", "example")
1671 .with_retry(Some(500))
1672 .with_detail_text("context");
1673
1674 assert!(err.retryable);
1675 assert_eq!(err.backoff_ms, Some(500));
1676 match err.detail() {
1677 Some(ErrorDetail::Text(detail)) => assert_eq!(detail, "context"),
1678 other => panic!("unexpected detail {other:?}"),
1679 }
1680 }
1681
1682 #[cfg(feature = "std")]
1683 #[test]
1684 fn node_error_source_roundtrips() {
1685 use std::io::Error;
1686
1687 let source = Error::other("boom");
1688 let err = NodeError::new("TEST", "example").with_source(source);
1689 assert!(err.source().is_some());
1690 }
1691}