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