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