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};
214#[deprecated(
215 since = "0.4.52",
216 note = "use schemas::component::v0_6_0::ComponentQaSpec"
217)]
218pub use schemas::component::v0_5_0::LegacyComponentQaSpec;
219pub use schemas::component::v0_6_0::{
220 ComponentDescribe, ComponentInfo, ComponentOperation as ComponentDescribeOperation,
221 ComponentQaSpec, ComponentRunInput, ComponentRunOutput, QaMode as ComponentQaMode,
222 Question as ComponentQuestion, QuestionKind as ComponentQuestionKind,
223 RedactionKind as ComponentRedactionKind, RedactionRule as ComponentRedactionRule,
224};
225pub use schemas::pack::v0_6_0::{
226 CapabilityDescriptor, CapabilityMetadata, PackDescribe, PackInfo, PackQaSpec,
227 PackValidationResult,
228};
229pub use secrets::{SecretFormat, SecretKey, SecretRequirement, SecretScope};
230pub use session::canonical_session_key;
231#[allow(deprecated)]
232pub use session::{ReplyScope, SessionCursor, SessionData, SessionKey, WaitScope};
233pub use state::{StateKey, StatePath};
234pub use store::{
235 ArtifactSelector, BundleSpec, CapabilityMap, Collection, ConnectionKind, DesiredState,
236 DesiredStateExportSpec, DesiredSubscriptionEntry, Environment, LayoutSection,
237 LayoutSectionKind, PackOrComponentRef, PlanLimits, PriceModel, ProductOverride, RolloutState,
238 RolloutStatus, StoreFront, StorePlan, StoreProduct, StoreProductKind, Subscription,
239 SubscriptionStatus, Theme, VersionStrategy,
240};
241pub use supply_chain::{
242 AttestationStatement, BuildPlan, BuildStatus, BuildStatusKind, MetadataRecord, PredicateType,
243 RepoContext, ScanKind, ScanRequest, ScanResult, ScanStatusKind, SignRequest, StoreContext,
244 VerifyRequest, VerifyResult,
245};
246#[cfg(feature = "otel-keys")]
247pub use telemetry::OtlpKeys;
248pub use telemetry::SpanContext;
249#[cfg(feature = "telemetry-autoinit")]
250pub use telemetry::TelemetryCtx;
251pub use tenant::{Impersonation, TenantIdentity};
252pub use tenant_config::{
253 DefaultPipeline, DidContext, DidService, DistributorTarget, EnabledPacks,
254 IdentityProviderOption, RepoAuth, RepoConfigFeatures, RepoSkin, RepoSkinLayout, RepoSkinLinks,
255 RepoSkinTheme, RepoTenantConfig, RepoWorkerPanel, StoreTarget, TenantDidDocument,
256 VerificationMethod,
257};
258pub use validate::{
259 Diagnostic, PackValidator, Severity, ValidationCounts, ValidationReport,
260 validate_pack_manifest_core,
261};
262pub use worker::{WorkerMessage, WorkerRequest, WorkerResponse};
263
264#[cfg(feature = "schemars")]
265use alloc::borrow::Cow;
266use alloc::{borrow::ToOwned, collections::BTreeMap, format, string::String, vec::Vec};
267use core::fmt;
268use core::str::FromStr;
269#[cfg(feature = "schemars")]
270use schemars::JsonSchema;
271use semver::VersionReq;
272#[cfg(feature = "time")]
273use time::OffsetDateTime;
274
275#[cfg(feature = "serde")]
276use serde::{Deserialize, Serialize};
277
278#[cfg(feature = "std")]
279use alloc::boxed::Box;
280
281#[cfg(feature = "std")]
282use std::error::Error as StdError;
283
284pub(crate) fn validate_identifier(value: &str, label: &str) -> GResult<()> {
286 if value.is_empty() {
287 return Err(GreenticError::new(
288 ErrorCode::InvalidInput,
289 format!("{label} must not be empty"),
290 ));
291 }
292 if value
293 .chars()
294 .any(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
295 {
296 return Err(GreenticError::new(
297 ErrorCode::InvalidInput,
298 format!("{label} must contain only ASCII letters, digits, '.', '-', or '_'"),
299 ));
300 }
301 Ok(())
302}
303
304pub(crate) fn validate_api_key_ref(value: &str) -> GResult<()> {
306 if value.trim().is_empty() {
307 return Err(GreenticError::new(
308 ErrorCode::InvalidInput,
309 "ApiKeyRef must not be empty",
310 ));
311 }
312 if value.chars().any(char::is_whitespace) {
313 return Err(GreenticError::new(
314 ErrorCode::InvalidInput,
315 "ApiKeyRef must not contain whitespace",
316 ));
317 }
318 if !value.is_ascii() {
319 return Err(GreenticError::new(
320 ErrorCode::InvalidInput,
321 "ApiKeyRef must contain only ASCII characters",
322 ));
323 }
324 Ok(())
325}
326
327pub mod ids {
329 pub const PACK_ID: &str =
331 "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-id.schema.json";
332 pub const COMPONENT_ID: &str =
334 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-id.schema.json";
335 pub const FLOW_ID: &str =
337 "https://greentic-ai.github.io/greentic-types/schemas/v1/flow-id.schema.json";
338 pub const NODE_ID: &str =
340 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-id.schema.json";
341 pub const TENANT_CONTEXT: &str =
343 "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-context.schema.json";
344 pub const HASH_DIGEST: &str =
346 "https://greentic-ai.github.io/greentic-types/schemas/v1/hash-digest.schema.json";
347 pub const SEMVER_REQ: &str =
349 "https://greentic-ai.github.io/greentic-types/schemas/v1/semver-req.schema.json";
350 pub const REDACTION_PATH: &str =
352 "https://greentic-ai.github.io/greentic-types/schemas/v1/redaction-path.schema.json";
353 pub const CAPABILITIES: &str =
355 "https://greentic-ai.github.io/greentic-types/schemas/v1/capabilities.schema.json";
356 pub const REPO_SKIN: &str =
358 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-skin.schema.json";
359 pub const REPO_AUTH: &str =
361 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-auth.schema.json";
362 pub const REPO_TENANT_CONFIG: &str =
364 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-tenant-config.schema.json";
365 pub const TENANT_DID_DOCUMENT: &str =
367 "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-did-document.schema.json";
368 pub const FLOW: &str = "greentic.flow.v1";
370 pub const FLOW_RESOLVE: &str = "greentic.flow.resolve.v1";
372 pub const FLOW_RESOLVE_SUMMARY: &str = "greentic.flow.resolve-summary.v1";
374 pub const NODE: &str =
376 "https://greentic-ai.github.io/greentic-types/schemas/v1/node.schema.json";
377 pub const COMPONENT_MANIFEST: &str =
379 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-manifest.schema.json";
380 pub const PACK_MANIFEST: &str = "greentic.pack-manifest.v1";
382 pub const VALIDATION_SEVERITY: &str =
384 "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-severity.schema.json";
385 pub const VALIDATION_DIAGNOSTIC: &str =
387 "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-diagnostic.schema.json";
388 pub const VALIDATION_REPORT: &str =
390 "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-report.schema.json";
391 pub const PROVIDER_MANIFEST: &str =
393 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-manifest.schema.json";
394 pub const PROVIDER_RUNTIME_REF: &str =
396 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-runtime-ref.schema.json";
397 pub const PROVIDER_DECL: &str =
399 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-decl.schema.json";
400 pub const PROVIDER_EXTENSION_INLINE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-extension-inline.schema.json";
402 pub const PROVIDER_INSTALL_RECORD: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-install-record.schema.json";
404 pub const LIMITS: &str =
406 "https://greentic-ai.github.io/greentic-types/schemas/v1/limits.schema.json";
407 pub const TELEMETRY_SPEC: &str =
409 "https://greentic-ai.github.io/greentic-types/schemas/v1/telemetry-spec.schema.json";
410 pub const NODE_SUMMARY: &str =
412 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-summary.schema.json";
413 pub const NODE_FAILURE: &str =
415 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-failure.schema.json";
416 pub const NODE_STATUS: &str =
418 "https://greentic-ai.github.io/greentic-types/schemas/v1/node-status.schema.json";
419 pub const RUN_STATUS: &str =
421 "https://greentic-ai.github.io/greentic-types/schemas/v1/run-status.schema.json";
422 pub const TRANSCRIPT_OFFSET: &str =
424 "https://greentic-ai.github.io/greentic-types/schemas/v1/transcript-offset.schema.json";
425 pub const TOOLS_CAPS: &str =
427 "https://greentic-ai.github.io/greentic-types/schemas/v1/tools-caps.schema.json";
428 pub const SECRETS_CAPS: &str =
430 "https://greentic-ai.github.io/greentic-types/schemas/v1/secrets-caps.schema.json";
431 pub const BRANCH_REF: &str =
433 "https://greentic-ai.github.io/greentic-types/schemas/v1/branch-ref.schema.json";
434 pub const COMMIT_REF: &str =
436 "https://greentic-ai.github.io/greentic-types/schemas/v1/commit-ref.schema.json";
437 pub const GIT_PROVIDER_REF: &str =
439 "https://greentic-ai.github.io/greentic-types/schemas/v1/git-provider-ref.schema.json";
440 pub const SCANNER_REF: &str =
442 "https://greentic-ai.github.io/greentic-types/schemas/v1/scanner-ref.schema.json";
443 pub const WEBHOOK_ID: &str =
445 "https://greentic-ai.github.io/greentic-types/schemas/v1/webhook-id.schema.json";
446 pub const PROVIDER_INSTALL_ID: &str =
448 "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-install-id.schema.json";
449 pub const REPO_REF: &str =
451 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-ref.schema.json";
452 pub const COMPONENT_REF: &str =
454 "https://greentic-ai.github.io/greentic-types/schemas/v1/component-ref.schema.json";
455 pub const VERSION_REF: &str =
457 "https://greentic-ai.github.io/greentic-types/schemas/v1/version-ref.schema.json";
458 pub const BUILD_REF: &str =
460 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-ref.schema.json";
461 pub const SCAN_REF: &str =
463 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-ref.schema.json";
464 pub const ATTESTATION_REF: &str =
466 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-ref.schema.json";
467 pub const ATTESTATION_ID: &str =
469 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-id.schema.json";
470 pub const POLICY_REF: &str =
472 "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-ref.schema.json";
473 pub const POLICY_INPUT_REF: &str =
475 "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-input-ref.schema.json";
476 pub const STORE_REF: &str =
478 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-ref.schema.json";
479 pub const REGISTRY_REF: &str =
481 "https://greentic-ai.github.io/greentic-types/schemas/v1/registry-ref.schema.json";
482 pub const OCI_IMAGE_REF: &str =
484 "https://greentic-ai.github.io/greentic-types/schemas/v1/oci-image-ref.schema.json";
485 pub const ARTIFACT_REF: &str =
487 "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-ref.schema.json";
488 pub const SBOM_REF: &str =
490 "https://greentic-ai.github.io/greentic-types/schemas/v1/sbom-ref.schema.json";
491 pub const SIGNING_KEY_REF: &str =
493 "https://greentic-ai.github.io/greentic-types/schemas/v1/signing-key-ref.schema.json";
494 pub const SIGNATURE_REF: &str =
496 "https://greentic-ai.github.io/greentic-types/schemas/v1/signature-ref.schema.json";
497 pub const STATEMENT_REF: &str =
499 "https://greentic-ai.github.io/greentic-types/schemas/v1/statement-ref.schema.json";
500 pub const BUILD_LOG_REF: &str =
502 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-log-ref.schema.json";
503 pub const METADATA_RECORD_REF: &str =
505 "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record-ref.schema.json";
506 pub const API_KEY_REF: &str =
508 "https://greentic-ai.github.io/greentic-types/schemas/v1/api-key-ref.schema.json";
509 pub const ENVIRONMENT_REF: &str =
511 "https://greentic-ai.github.io/greentic-types/schemas/v1/environment-ref.schema.json";
512 pub const DISTRIBUTOR_REF: &str =
514 "https://greentic-ai.github.io/greentic-types/schemas/v1/distributor-ref.schema.json";
515 pub const STOREFRONT_ID: &str =
517 "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront-id.schema.json";
518 pub const STORE_PRODUCT_ID: &str =
520 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-id.schema.json";
521 pub const STORE_PLAN_ID: &str =
523 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan-id.schema.json";
524 pub const SUBSCRIPTION_ID: &str =
526 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-id.schema.json";
527 pub const BUNDLE_ID: &str =
529 "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle-id.schema.json";
530 pub const COLLECTION_ID: &str =
532 "https://greentic-ai.github.io/greentic-types/schemas/v1/collection-id.schema.json";
533 pub const ARTIFACT_SELECTOR: &str =
535 "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-selector.schema.json";
536 pub const CAPABILITY_MAP: &str =
538 "https://greentic-ai.github.io/greentic-types/schemas/v1/capability-map.schema.json";
539 pub const STORE_PRODUCT_KIND: &str =
541 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-kind.schema.json";
542 pub const VERSION_STRATEGY: &str =
544 "https://greentic-ai.github.io/greentic-types/schemas/v1/version-strategy.schema.json";
545 pub const ROLLOUT_STATUS: &str =
547 "https://greentic-ai.github.io/greentic-types/schemas/v1/rollout-status.schema.json";
548 pub const CONNECTION_KIND: &str =
550 "https://greentic-ai.github.io/greentic-types/schemas/v1/connection-kind.schema.json";
551 pub const PACK_OR_COMPONENT_REF: &str =
553 "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-or-component-ref.schema.json";
554 pub const PLAN_LIMITS: &str =
556 "https://greentic-ai.github.io/greentic-types/schemas/v1/plan-limits.schema.json";
557 pub const PRICE_MODEL: &str =
559 "https://greentic-ai.github.io/greentic-types/schemas/v1/price-model.schema.json";
560 pub const SUBSCRIPTION_STATUS: &str =
562 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-status.schema.json";
563 pub const BUILD_PLAN: &str =
565 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-plan.schema.json";
566 pub const BUILD_STATUS: &str =
568 "https://greentic-ai.github.io/greentic-types/schemas/v1/build-status.schema.json";
569 pub const SCAN_REQUEST: &str =
571 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-request.schema.json";
572 pub const SCAN_RESULT: &str =
574 "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-result.schema.json";
575 pub const SIGN_REQUEST: &str =
577 "https://greentic-ai.github.io/greentic-types/schemas/v1/sign-request.schema.json";
578 pub const VERIFY_REQUEST: &str =
580 "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-request.schema.json";
581 pub const VERIFY_RESULT: &str =
583 "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-result.schema.json";
584 pub const ATTESTATION_STATEMENT: &str =
586 "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-statement.schema.json";
587 pub const METADATA_RECORD: &str =
589 "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record.schema.json";
590 pub const REPO_CONTEXT: &str =
592 "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-context.schema.json";
593 pub const STORE_CONTEXT: &str =
595 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-context.schema.json";
596 pub const BUNDLE: &str =
598 "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle.schema.json";
599 pub const DESIRED_STATE_EXPORT: &str =
601 "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state-export.schema.json";
602 pub const DESIRED_STATE: &str =
604 "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state.schema.json";
605 pub const DESIRED_SUBSCRIPTION_ENTRY: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-subscription-entry.schema.json";
607 pub const STOREFRONT: &str =
609 "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront.schema.json";
610 pub const STORE_PRODUCT: &str =
612 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product.schema.json";
613 pub const STORE_PLAN: &str =
615 "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan.schema.json";
616 pub const SUBSCRIPTION: &str =
618 "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription.schema.json";
619 pub const ENVIRONMENT: &str =
621 "https://greentic-ai.github.io/greentic-types/schemas/v1/environment.schema.json";
622 pub const THEME: &str =
624 "https://greentic-ai.github.io/greentic-types/schemas/v1/theme.schema.json";
625 pub const LAYOUT_SECTION: &str =
627 "https://greentic-ai.github.io/greentic-types/schemas/v1/layout-section.schema.json";
628 pub const COLLECTION: &str =
630 "https://greentic-ai.github.io/greentic-types/schemas/v1/collection.schema.json";
631 pub const PRODUCT_OVERRIDE: &str =
633 "https://greentic-ai.github.io/greentic-types/schemas/v1/product-override.schema.json";
634 pub const EVENT_ENVELOPE: &str =
636 "https://greentic-ai.github.io/greentic-types/schemas/v1/event-envelope.schema.json";
637 pub const EVENT_PROVIDER_DESCRIPTOR: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/event-provider-descriptor.schema.json";
639 pub const CHANNEL_MESSAGE_ENVELOPE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/channel-message-envelope.schema.json";
641 pub const ATTACHMENT: &str =
643 "https://greentic-ai.github.io/greentic-types/schemas/v1/attachment.schema.json";
644 pub const WORKER_REQUEST: &str =
646 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-request.schema.json";
647 pub const WORKER_MESSAGE: &str =
649 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-message.schema.json";
650 pub const WORKER_RESPONSE: &str =
652 "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-response.schema.json";
653 pub const OTLP_KEYS: &str =
655 "https://greentic-ai.github.io/greentic-types/schemas/v1/otlp-keys.schema.json";
656 pub const RUN_RESULT: &str =
658 "https://greentic-ai.github.io/greentic-types/schemas/v1/run-result.schema.json";
659}
660
661#[cfg(all(feature = "schema", feature = "std"))]
662pub fn write_all_schemas(out_dir: &std::path::Path) -> anyhow::Result<()> {
664 use anyhow::Context;
665 use std::fs;
666
667 fs::create_dir_all(out_dir)
668 .with_context(|| format!("failed to create {}", out_dir.display()))?;
669
670 for entry in crate::schema::entries() {
671 let schema = (entry.generator)();
672 let path = out_dir.join(entry.file_name);
673 if let Some(parent) = path.parent() {
674 fs::create_dir_all(parent)
675 .with_context(|| format!("failed to create {}", parent.display()))?;
676 }
677
678 let json =
679 serde_json::to_vec_pretty(&schema).context("failed to serialize schema to JSON")?;
680 fs::write(&path, json).with_context(|| format!("failed to write {}", path.display()))?;
681 }
682
683 Ok(())
684}
685
686macro_rules! id_newtype {
687 ($name:ident, $doc:literal) => {
688 #[doc = $doc]
689 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
690 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
691 #[cfg_attr(feature = "schemars", derive(JsonSchema))]
692 #[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
693 pub struct $name(pub String);
694
695 impl $name {
696 pub fn as_str(&self) -> &str {
698 &self.0
699 }
700
701 pub fn new(value: impl AsRef<str>) -> GResult<Self> {
703 value.as_ref().parse()
704 }
705 }
706
707 impl From<$name> for String {
708 fn from(value: $name) -> Self {
709 value.0
710 }
711 }
712
713 impl AsRef<str> for $name {
714 fn as_ref(&self) -> &str {
715 self.as_str()
716 }
717 }
718
719 impl fmt::Display for $name {
720 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
721 f.write_str(self.as_str())
722 }
723 }
724
725 impl FromStr for $name {
726 type Err = GreenticError;
727
728 fn from_str(value: &str) -> Result<Self, Self::Err> {
729 validate_identifier(value, stringify!($name))?;
730 Ok(Self(value.to_owned()))
731 }
732 }
733
734 impl TryFrom<String> for $name {
735 type Error = GreenticError;
736
737 fn try_from(value: String) -> Result<Self, Self::Error> {
738 $name::from_str(&value)
739 }
740 }
741
742 impl TryFrom<&str> for $name {
743 type Error = GreenticError;
744
745 fn try_from(value: &str) -> Result<Self, Self::Error> {
746 $name::from_str(value)
747 }
748 }
749 };
750}
751
752id_newtype!(EnvId, "Environment identifier for a tenant context.");
753id_newtype!(TenantId, "Tenant identifier within an environment.");
754id_newtype!(TeamId, "Team identifier belonging to a tenant.");
755id_newtype!(UserId, "User identifier within a tenant.");
756id_newtype!(BranchRef, "Reference to a source control branch.");
757id_newtype!(CommitRef, "Reference to a source control commit.");
758id_newtype!(
759 GitProviderRef,
760 "Identifier referencing a source control provider."
761);
762id_newtype!(ScannerRef, "Identifier referencing a scanner provider.");
763id_newtype!(WebhookId, "Identifier referencing a registered webhook.");
764id_newtype!(
765 ProviderInstallId,
766 "Identifier referencing a provider installation record."
767);
768id_newtype!(PackId, "Globally unique pack identifier.");
769id_newtype!(
770 ComponentId,
771 "Identifier referencing a component binding in a pack."
772);
773id_newtype!(FlowId, "Identifier referencing a flow inside a pack.");
774id_newtype!(NodeId, "Identifier referencing a node inside a flow graph.");
775id_newtype!(
776 EnvironmentRef,
777 "Identifier referencing a deployment environment."
778);
779id_newtype!(
780 DistributorRef,
781 "Identifier referencing a distributor instance."
782);
783id_newtype!(StoreFrontId, "Identifier referencing a storefront.");
784id_newtype!(
785 StoreProductId,
786 "Identifier referencing a product in the store catalog."
787);
788id_newtype!(
789 StorePlanId,
790 "Identifier referencing a plan for a store product."
791);
792id_newtype!(
793 SubscriptionId,
794 "Identifier referencing a subscription entry."
795);
796id_newtype!(BundleId, "Identifier referencing a distributor bundle.");
797id_newtype!(CollectionId, "Identifier referencing a product collection.");
798id_newtype!(RepoRef, "Repository reference within a supply chain.");
799id_newtype!(
800 ComponentRef,
801 "Supply-chain component reference (distinct from pack ComponentId)."
802);
803id_newtype!(
804 VersionRef,
805 "Version reference for a component or metadata record."
806);
807id_newtype!(BuildRef, "Build reference within a supply chain.");
808id_newtype!(ScanRef, "Scan reference within a supply chain.");
809id_newtype!(
810 AttestationRef,
811 "Attestation reference within a supply chain."
812);
813id_newtype!(AttestationId, "Identifier referencing an attestation.");
814id_newtype!(PolicyRef, "Policy reference within a supply chain.");
815id_newtype!(
816 PolicyInputRef,
817 "Reference to a policy input payload for evaluation."
818);
819id_newtype!(StoreRef, "Content store reference within a supply chain.");
820id_newtype!(
821 RegistryRef,
822 "Registry reference for OCI or artifact storage."
823);
824id_newtype!(
825 OciImageRef,
826 "Reference to an OCI image for distribution (oci://repo/name:tag or oci://repo/name@sha256:...)."
827);
828id_newtype!(
829 ArtifactRef,
830 "Artifact reference within a build or scan result."
831);
832id_newtype!(
833 SbomRef,
834 "Reference to a Software Bill of Materials artifact."
835);
836id_newtype!(SigningKeyRef, "Reference to a signing key handle.");
837id_newtype!(SignatureRef, "Reference to a generated signature.");
838id_newtype!(StatementRef, "Reference to an attestation statement.");
839id_newtype!(
840 BuildLogRef,
841 "Reference to a build log output produced during execution."
842);
843id_newtype!(
844 MetadataRecordRef,
845 "Reference to a metadata record attached to artifacts or bundles."
846);
847
848#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
850#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
851#[cfg_attr(feature = "schemars", derive(JsonSchema))]
852#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
853pub struct ApiKeyRef(pub String);
854
855impl ApiKeyRef {
856 pub fn as_str(&self) -> &str {
858 &self.0
859 }
860
861 pub fn new(value: impl AsRef<str>) -> GResult<Self> {
863 value.as_ref().parse()
864 }
865}
866
867impl fmt::Display for ApiKeyRef {
868 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
869 f.write_str(self.as_str())
870 }
871}
872
873impl FromStr for ApiKeyRef {
874 type Err = GreenticError;
875
876 fn from_str(value: &str) -> Result<Self, Self::Err> {
877 validate_api_key_ref(value)?;
878 Ok(Self(value.to_owned()))
879 }
880}
881
882impl TryFrom<String> for ApiKeyRef {
883 type Error = GreenticError;
884
885 fn try_from(value: String) -> Result<Self, Self::Error> {
886 ApiKeyRef::from_str(&value)
887 }
888}
889
890impl TryFrom<&str> for ApiKeyRef {
891 type Error = GreenticError;
892
893 fn try_from(value: &str) -> Result<Self, Self::Error> {
894 ApiKeyRef::from_str(value)
895 }
896}
897
898impl From<ApiKeyRef> for String {
899 fn from(value: ApiKeyRef) -> Self {
900 value.0
901 }
902}
903
904impl AsRef<str> for ApiKeyRef {
905 fn as_ref(&self) -> &str {
906 self.as_str()
907 }
908}
909
910#[derive(Clone, Debug, PartialEq, Eq, Hash)]
912#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
913#[cfg_attr(feature = "schemars", derive(JsonSchema))]
914pub struct TenantContext {
915 pub tenant_id: TenantId,
917 #[cfg_attr(
919 feature = "serde",
920 serde(default, skip_serializing_if = "Option::is_none")
921 )]
922 pub team_id: Option<TeamId>,
923 #[cfg_attr(
925 feature = "serde",
926 serde(default, skip_serializing_if = "Option::is_none")
927 )]
928 pub user_id: Option<UserId>,
929 #[cfg_attr(
931 feature = "serde",
932 serde(default, skip_serializing_if = "Option::is_none")
933 )]
934 pub session_id: Option<String>,
935 #[cfg_attr(
937 feature = "serde",
938 serde(default, skip_serializing_if = "BTreeMap::is_empty")
939 )]
940 pub attributes: BTreeMap<String, String>,
941}
942
943impl TenantContext {
944 pub fn new(tenant_id: TenantId) -> Self {
946 Self {
947 tenant_id,
948 team_id: None,
949 user_id: None,
950 session_id: None,
951 attributes: BTreeMap::new(),
952 }
953 }
954}
955
956impl From<&TenantCtx> for TenantContext {
957 fn from(ctx: &TenantCtx) -> Self {
958 Self {
959 tenant_id: ctx.tenant_id.clone(),
960 team_id: ctx.team_id.clone().or_else(|| ctx.team.clone()),
961 user_id: ctx.user_id.clone().or_else(|| ctx.user.clone()),
962 session_id: ctx.session_id.clone(),
963 attributes: ctx.attributes.clone(),
964 }
965 }
966}
967
968#[derive(Clone, Debug, PartialEq, Eq, Hash)]
970#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
971#[cfg_attr(feature = "schemars", derive(JsonSchema))]
972#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
973pub enum HashAlgorithm {
974 Blake3,
976 Other(String),
978}
979
980#[derive(Clone, Debug, PartialEq, Eq, Hash)]
982#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
983#[cfg_attr(
984 feature = "serde",
985 serde(into = "HashDigestRepr", try_from = "HashDigestRepr")
986)]
987#[cfg_attr(feature = "schemars", derive(JsonSchema))]
988pub struct HashDigest {
989 pub algo: HashAlgorithm,
991 pub hex: String,
993}
994
995impl HashDigest {
996 pub fn new(algo: HashAlgorithm, hex: impl Into<String>) -> GResult<Self> {
998 let hex = hex.into();
999 validate_hex(&hex)?;
1000 Ok(Self { algo, hex })
1001 }
1002
1003 pub fn blake3(hex: impl Into<String>) -> GResult<Self> {
1005 Self::new(HashAlgorithm::Blake3, hex)
1006 }
1007}
1008
1009#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1010#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1011struct HashDigestRepr {
1012 algo: HashAlgorithm,
1013 hex: String,
1014}
1015
1016impl From<HashDigest> for HashDigestRepr {
1017 fn from(value: HashDigest) -> Self {
1018 Self {
1019 algo: value.algo,
1020 hex: value.hex,
1021 }
1022 }
1023}
1024
1025impl TryFrom<HashDigestRepr> for HashDigest {
1026 type Error = GreenticError;
1027
1028 fn try_from(value: HashDigestRepr) -> Result<Self, Self::Error> {
1029 HashDigest::new(value.algo, value.hex)
1030 }
1031}
1032
1033fn validate_hex(hex: &str) -> GResult<()> {
1034 if hex.is_empty() {
1035 return Err(GreenticError::new(
1036 ErrorCode::InvalidInput,
1037 "digest hex payload must not be empty",
1038 ));
1039 }
1040 if !hex.len().is_multiple_of(2) {
1041 return Err(GreenticError::new(
1042 ErrorCode::InvalidInput,
1043 "digest hex payload must have an even number of digits",
1044 ));
1045 }
1046 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
1047 return Err(GreenticError::new(
1048 ErrorCode::InvalidInput,
1049 "digest hex payload must be hexadecimal",
1050 ));
1051 }
1052 Ok(())
1053}
1054
1055#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1057#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1058#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1059pub struct SemverReq(String);
1060
1061impl SemverReq {
1062 pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1064 let value = value.as_ref();
1065 VersionReq::parse(value).map_err(|err| {
1066 GreenticError::new(
1067 ErrorCode::InvalidInput,
1068 format!("invalid semver requirement '{value}': {err}"),
1069 )
1070 })?;
1071 Ok(Self(value.to_owned()))
1072 }
1073
1074 pub fn as_str(&self) -> &str {
1076 &self.0
1077 }
1078
1079 pub fn to_version_req(&self) -> VersionReq {
1081 VersionReq::parse(&self.0)
1082 .unwrap_or_else(|err| unreachable!("SemverReq::parse validated inputs: {err}"))
1083 }
1084}
1085
1086impl fmt::Display for SemverReq {
1087 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1088 f.write_str(self.as_str())
1089 }
1090}
1091
1092impl From<SemverReq> for String {
1093 fn from(value: SemverReq) -> Self {
1094 value.0
1095 }
1096}
1097
1098impl TryFrom<String> for SemverReq {
1099 type Error = GreenticError;
1100
1101 fn try_from(value: String) -> Result<Self, Self::Error> {
1102 SemverReq::parse(&value)
1103 }
1104}
1105
1106impl TryFrom<&str> for SemverReq {
1107 type Error = GreenticError;
1108
1109 fn try_from(value: &str) -> Result<Self, Self::Error> {
1110 SemverReq::parse(value)
1111 }
1112}
1113
1114impl FromStr for SemverReq {
1115 type Err = GreenticError;
1116
1117 fn from_str(s: &str) -> Result<Self, Self::Err> {
1118 SemverReq::parse(s)
1119 }
1120}
1121
1122#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1124#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1125#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1126pub struct RedactionPath(String);
1127
1128impl RedactionPath {
1129 pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1131 let value = value.as_ref();
1132 validate_jsonpath(value)?;
1133 Ok(Self(value.to_owned()))
1134 }
1135
1136 pub fn as_str(&self) -> &str {
1138 &self.0
1139 }
1140}
1141
1142impl fmt::Display for RedactionPath {
1143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1144 f.write_str(self.as_str())
1145 }
1146}
1147
1148impl From<RedactionPath> for String {
1149 fn from(value: RedactionPath) -> Self {
1150 value.0
1151 }
1152}
1153
1154impl TryFrom<String> for RedactionPath {
1155 type Error = GreenticError;
1156
1157 fn try_from(value: String) -> Result<Self, Self::Error> {
1158 RedactionPath::parse(&value)
1159 }
1160}
1161
1162impl TryFrom<&str> for RedactionPath {
1163 type Error = GreenticError;
1164
1165 fn try_from(value: &str) -> Result<Self, Self::Error> {
1166 RedactionPath::parse(value)
1167 }
1168}
1169
1170fn validate_jsonpath(path: &str) -> GResult<()> {
1171 if path.is_empty() {
1172 return Err(GreenticError::new(
1173 ErrorCode::InvalidInput,
1174 "redaction path cannot be empty",
1175 ));
1176 }
1177 if !path.starts_with('$') {
1178 return Err(GreenticError::new(
1179 ErrorCode::InvalidInput,
1180 "redaction path must start with '$'",
1181 ));
1182 }
1183 if path.chars().any(|c| c.is_control()) {
1184 return Err(GreenticError::new(
1185 ErrorCode::InvalidInput,
1186 "redaction path cannot contain control characters",
1187 ));
1188 }
1189 Ok(())
1190}
1191
1192#[cfg(feature = "schemars")]
1193impl JsonSchema for SemverReq {
1194 fn schema_name() -> Cow<'static, str> {
1195 Cow::Borrowed("SemverReq")
1196 }
1197
1198 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1199 let mut schema = <String>::json_schema(generator);
1200 if schema.get("description").is_none() {
1201 schema.insert(
1202 "description".into(),
1203 "Validated semantic version requirement string".into(),
1204 );
1205 }
1206 schema
1207 }
1208}
1209
1210#[cfg(feature = "schemars")]
1211impl JsonSchema for RedactionPath {
1212 fn schema_name() -> Cow<'static, str> {
1213 Cow::Borrowed("RedactionPath")
1214 }
1215
1216 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1217 let mut schema = <String>::json_schema(generator);
1218 if schema.get("description").is_none() {
1219 schema.insert(
1220 "description".into(),
1221 "JSONPath expression used for runtime redaction".into(),
1222 );
1223 }
1224 schema
1225 }
1226}
1227
1228#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1230#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1231#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1232pub struct InvocationDeadline {
1233 unix_millis: i128,
1234}
1235
1236impl InvocationDeadline {
1237 pub const fn from_unix_millis(unix_millis: i128) -> Self {
1239 Self { unix_millis }
1240 }
1241
1242 pub const fn unix_millis(&self) -> i128 {
1244 self.unix_millis
1245 }
1246
1247 #[cfg(feature = "time")]
1249 pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, time::error::ComponentRange> {
1250 OffsetDateTime::from_unix_timestamp_nanos(self.unix_millis * 1_000_000)
1251 }
1252
1253 #[cfg(feature = "time")]
1255 pub fn from_offset_date_time(value: OffsetDateTime) -> Self {
1256 let nanos = value.unix_timestamp_nanos();
1257 Self {
1258 unix_millis: nanos / 1_000_000,
1259 }
1260 }
1261}
1262
1263#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1265#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1266#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1267pub struct TenantCtx {
1268 pub env: EnvId,
1270 pub tenant: TenantId,
1272 pub tenant_id: TenantId,
1274 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1276 pub team: Option<TeamId>,
1277 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1279 pub team_id: Option<TeamId>,
1280 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1282 pub user: Option<UserId>,
1283 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1285 pub user_id: Option<UserId>,
1286 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1288 pub session_id: Option<String>,
1289 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1291 pub flow_id: Option<String>,
1292 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1294 pub node_id: Option<String>,
1295 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1297 pub provider_id: Option<String>,
1298 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1300 pub trace_id: Option<String>,
1301 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1303 pub i18n_id: Option<String>,
1304 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1306 pub correlation_id: Option<String>,
1307 #[cfg_attr(
1309 feature = "serde",
1310 serde(default, skip_serializing_if = "BTreeMap::is_empty")
1311 )]
1312 pub attributes: BTreeMap<String, String>,
1313 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1315 pub deadline: Option<InvocationDeadline>,
1316 pub attempt: u32,
1318 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1320 pub idempotency_key: Option<String>,
1321 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1323 pub impersonation: Option<Impersonation>,
1324}
1325
1326impl TenantCtx {
1327 pub fn new(env: EnvId, tenant: TenantId) -> Self {
1329 let tenant_id = tenant.clone();
1330 Self {
1331 env,
1332 tenant: tenant.clone(),
1333 tenant_id,
1334 team: None,
1335 team_id: None,
1336 user: None,
1337 user_id: None,
1338 session_id: None,
1339 flow_id: None,
1340 node_id: None,
1341 provider_id: None,
1342 trace_id: None,
1343 i18n_id: None,
1344 correlation_id: None,
1345 attributes: BTreeMap::new(),
1346 deadline: None,
1347 attempt: 0,
1348 idempotency_key: None,
1349 impersonation: None,
1350 }
1351 }
1352
1353 pub fn with_team(mut self, team: Option<TeamId>) -> Self {
1355 self.team = team.clone();
1356 self.team_id = team;
1357 self
1358 }
1359
1360 pub fn with_user(mut self, user: Option<UserId>) -> Self {
1362 self.user = user.clone();
1363 self.user_id = user;
1364 self
1365 }
1366
1367 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1369 self.session_id = Some(session.into());
1370 self
1371 }
1372
1373 pub fn with_flow(mut self, flow: impl Into<String>) -> Self {
1375 self.flow_id = Some(flow.into());
1376 self
1377 }
1378
1379 pub fn with_node(mut self, node: impl Into<String>) -> Self {
1381 self.node_id = Some(node.into());
1382 self
1383 }
1384
1385 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1387 self.provider_id = Some(provider.into());
1388 self
1389 }
1390
1391 pub fn with_attributes(mut self, attributes: BTreeMap<String, String>) -> Self {
1393 self.attributes = attributes;
1394 self
1395 }
1396
1397 pub fn with_impersonation(mut self, impersonation: Option<Impersonation>) -> Self {
1399 self.impersonation = impersonation;
1400 self
1401 }
1402
1403 pub fn with_attempt(mut self, attempt: u32) -> Self {
1405 self.attempt = attempt;
1406 self
1407 }
1408
1409 pub fn with_deadline(mut self, deadline: Option<InvocationDeadline>) -> Self {
1411 self.deadline = deadline;
1412 self
1413 }
1414
1415 pub fn session_id(&self) -> Option<&str> {
1417 self.session_id.as_deref()
1418 }
1419
1420 pub fn flow_id(&self) -> Option<&str> {
1422 self.flow_id.as_deref()
1423 }
1424
1425 pub fn node_id(&self) -> Option<&str> {
1427 self.node_id.as_deref()
1428 }
1429
1430 pub fn provider_id(&self) -> Option<&str> {
1432 self.provider_id.as_deref()
1433 }
1434}
1435
1436pub type BinaryPayload = Vec<u8>;
1438
1439#[derive(Clone, Debug, PartialEq, Eq)]
1441#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1442#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1443pub struct InvocationEnvelope {
1444 pub ctx: TenantCtx,
1446 pub flow_id: String,
1448 pub node_id: Option<String>,
1450 pub op: String,
1452 pub payload: BinaryPayload,
1454 pub metadata: BinaryPayload,
1456}
1457
1458#[derive(Clone, Debug, PartialEq, Eq)]
1460#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1461#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1462pub enum ErrorDetail {
1463 Text(String),
1465 Binary(BinaryPayload),
1467}
1468
1469#[derive(Debug)]
1471#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1472#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1473pub struct NodeError {
1474 pub code: String,
1476 pub message: String,
1478 pub retryable: bool,
1480 pub backoff_ms: Option<u64>,
1482 pub details: Option<ErrorDetail>,
1484 #[cfg(feature = "std")]
1485 #[cfg_attr(feature = "serde", serde(skip, default = "default_source"))]
1486 #[cfg_attr(feature = "schemars", schemars(skip))]
1487 source: Option<Box<dyn StdError + Send + Sync>>,
1488}
1489
1490#[cfg(all(feature = "std", feature = "serde"))]
1491fn default_source() -> Option<Box<dyn StdError + Send + Sync>> {
1492 None
1493}
1494
1495impl NodeError {
1496 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
1498 Self {
1499 code: code.into(),
1500 message: message.into(),
1501 retryable: false,
1502 backoff_ms: None,
1503 details: None,
1504 #[cfg(feature = "std")]
1505 source: None,
1506 }
1507 }
1508
1509 pub fn with_retry(mut self, backoff_ms: Option<u64>) -> Self {
1511 self.retryable = true;
1512 self.backoff_ms = backoff_ms;
1513 self
1514 }
1515
1516 pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
1518 self.details = Some(detail);
1519 self
1520 }
1521
1522 pub fn with_detail_text(mut self, detail: impl Into<String>) -> Self {
1524 self.details = Some(ErrorDetail::Text(detail.into()));
1525 self
1526 }
1527
1528 pub fn with_detail_binary(mut self, detail: BinaryPayload) -> Self {
1530 self.details = Some(ErrorDetail::Binary(detail));
1531 self
1532 }
1533
1534 #[cfg(feature = "std")]
1535 pub fn with_source<E>(mut self, source: E) -> Self
1537 where
1538 E: StdError + Send + Sync + 'static,
1539 {
1540 self.source = Some(Box::new(source));
1541 self
1542 }
1543
1544 pub fn detail(&self) -> Option<&ErrorDetail> {
1546 self.details.as_ref()
1547 }
1548
1549 #[cfg(feature = "std")]
1550 pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
1552 self.source.as_deref()
1553 }
1554}
1555
1556impl fmt::Display for NodeError {
1557 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1558 write!(f, "{}: {}", self.code, self.message)
1559 }
1560}
1561
1562#[cfg(feature = "std")]
1563impl StdError for NodeError {
1564 fn source(&self) -> Option<&(dyn StdError + 'static)> {
1565 self.source
1566 .as_ref()
1567 .map(|err| err.as_ref() as &(dyn StdError + 'static))
1568 }
1569}
1570
1571pub type NodeResult<T> = Result<T, NodeError>;
1573
1574pub fn make_idempotency_key(
1579 ctx: &TenantCtx,
1580 flow_id: &str,
1581 node_id: Option<&str>,
1582 correlation: Option<&str>,
1583) -> String {
1584 let node_segment = node_id.unwrap_or_default();
1585 let correlation_segment = correlation
1586 .or(ctx.correlation_id.as_deref())
1587 .unwrap_or_default();
1588 let input = format!(
1589 "{}|{}|{}|{}",
1590 ctx.tenant_id.as_str(),
1591 flow_id,
1592 node_segment,
1593 correlation_segment
1594 );
1595 fnv1a_128_hex(input.as_bytes())
1596}
1597
1598const FNV_OFFSET_BASIS: u128 = 0x6c62272e07bb014262b821756295c58d;
1599const FNV_PRIME: u128 = 0x0000000001000000000000000000013b;
1600
1601fn fnv1a_128_hex(bytes: &[u8]) -> String {
1602 let mut hash = FNV_OFFSET_BASIS;
1603 for &byte in bytes {
1604 hash ^= byte as u128;
1605 hash = hash.wrapping_mul(FNV_PRIME);
1606 }
1607
1608 let mut output = String::with_capacity(32);
1609 for shift in (0..32).rev() {
1610 let nibble = ((hash >> (shift * 4)) & 0x0f) as u8;
1611 output.push(match nibble {
1612 0..=9 => (b'0' + nibble) as char,
1613 _ => (b'a' + (nibble - 10)) as char,
1614 });
1615 }
1616 output
1617}
1618
1619#[cfg(test)]
1620mod tests {
1621 use super::*;
1622 use core::convert::TryFrom;
1623 use time::OffsetDateTime;
1624
1625 fn sample_ctx() -> TenantCtx {
1626 let env = EnvId::try_from("prod").unwrap_or_else(|err| panic!("{err}"));
1627 let tenant = TenantId::try_from("tenant-123").unwrap_or_else(|err| panic!("{err}"));
1628 let team = TeamId::try_from("team-456").unwrap_or_else(|err| panic!("{err}"));
1629 let user = UserId::try_from("user-789").unwrap_or_else(|err| panic!("{err}"));
1630
1631 let mut ctx = TenantCtx::new(env, tenant)
1632 .with_team(Some(team))
1633 .with_user(Some(user))
1634 .with_attempt(2)
1635 .with_deadline(Some(InvocationDeadline::from_unix_millis(
1636 1_700_000_000_000,
1637 )));
1638 ctx.trace_id = Some("trace-abc".to_owned());
1639 ctx.correlation_id = Some("corr-xyz".to_owned());
1640 ctx.idempotency_key = Some("key-123".to_owned());
1641 ctx
1642 }
1643
1644 #[test]
1645 fn idempotent_key_stable() {
1646 let ctx = sample_ctx();
1647 let key_a = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1648 let key_b = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1649 assert_eq!(key_a, key_b);
1650 assert_eq!(key_a.len(), 32);
1651 }
1652
1653 #[test]
1654 fn idempotent_key_uses_context_correlation() {
1655 let ctx = sample_ctx();
1656 let key = make_idempotency_key(&ctx, "flow-1", None, None);
1657 let expected = make_idempotency_key(&ctx, "flow-1", None, ctx.correlation_id.as_deref());
1658 assert_eq!(key, expected);
1659 }
1660
1661 #[test]
1662 #[cfg(feature = "time")]
1663 fn deadline_roundtrips_through_offset_datetime() {
1664 let dt = OffsetDateTime::from_unix_timestamp(1_700_000_000)
1665 .unwrap_or_else(|err| panic!("valid timestamp: {err}"));
1666 let deadline = InvocationDeadline::from_offset_date_time(dt);
1667 let roundtrip = deadline
1668 .to_offset_date_time()
1669 .unwrap_or_else(|err| panic!("round-trip conversion failed: {err}"));
1670 let millis = dt.unix_timestamp_nanos() / 1_000_000;
1671 assert_eq!(deadline.unix_millis(), millis);
1672 assert_eq!(roundtrip.unix_timestamp_nanos() / 1_000_000, millis);
1673 }
1674
1675 #[test]
1676 fn node_error_builder_sets_fields() {
1677 let err = NodeError::new("TEST", "example")
1678 .with_retry(Some(500))
1679 .with_detail_text("context");
1680
1681 assert!(err.retryable);
1682 assert_eq!(err.backoff_ms, Some(500));
1683 match err.detail() {
1684 Some(ErrorDetail::Text(detail)) => assert_eq!(detail, "context"),
1685 other => panic!("unexpected detail {other:?}"),
1686 }
1687 }
1688
1689 #[cfg(feature = "std")]
1690 #[test]
1691 fn node_error_source_roundtrips() {
1692 use std::io::Error;
1693
1694 let source = Error::other("boom");
1695 let err = NodeError::new("TEST", "example").with_source(source);
1696 assert!(err.source().is_some());
1697 }
1698}