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