Skip to main content

greentic_types/
lib.rs

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
6//! Shared types and helpers for Greentic multi-tenant flows.
7//!
8//! # Overview
9//! Greentic components share a single crate for tenancy, execution outcomes, network limits, and
10//! schema metadata. Use the strongly-typed identifiers to keep flows, packs, and components
11//! consistent across repositories and to benefit from serde + schema validation automatically.
12//!
13//! Component manifests now support optional development-time flows in `dev_flows`. These are JSON
14//! FlowIR structures used only during authoring (`greentic-dev`, `greentic-component`). Runtimes
15//! may safely ignore this field.
16//!
17//! ## Tenant contexts
18//! ```
19//! use greentic_types::{EnvId, TenantCtx, TenantId};
20//!
21//! let ctx = TenantCtx::new("prod".parse().unwrap(), "tenant-42".parse().unwrap())
22//!     .with_team(Some("team-ops".parse().unwrap()))
23//!     .with_user(Some("agent-007".parse().unwrap()));
24//! assert_eq!(ctx.tenant_id.as_str(), "tenant-42");
25//! ```
26//!
27//! ## Run results & serialization
28//! ```
29//! # #[cfg(feature = "time")] {
30//! use greentic_types::{FlowId, PackId, RunResult, RunStatus, SessionKey};
31//! use semver::Version;
32//! use time::OffsetDateTime;
33//!
34//! let now = OffsetDateTime::UNIX_EPOCH;
35//! let result = RunResult {
36//!     session_id: SessionKey::from("sess-1"),
37//!     pack_id: "greentic.demo.pack".parse().unwrap(),
38//!     pack_version: Version::parse("1.0.0").unwrap(),
39//!     flow_id: "demo-flow".parse().unwrap(),
40//!     started_at_utc: now,
41//!     finished_at_utc: now,
42//!     status: RunStatus::Success,
43//!     node_summaries: Vec::new(),
44//!     failures: Vec::new(),
45//!     artifacts_dir: None,
46//! };
47//! println!("{}", serde_json::to_string_pretty(&result).unwrap());
48//! # }
49//! ```
50//!
51//! Published JSON Schemas are listed in [`SCHEMAS.md`](SCHEMAS.md) and hosted under
52//! <https://greentic-ai.github.io/greentic-types/schemas/v1/>.
53
54extern crate alloc;
55
56/// Crate version string exposed for telemetry and capability negotiation.
57pub const VERSION: &str = env!("CARGO_PKG_VERSION");
58/// Base URL for all published JSON Schemas.
59pub const SCHEMA_BASE_URL: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1";
60
61pub mod bindings;
62pub mod capabilities;
63#[cfg(feature = "std")]
64pub mod cbor;
65pub mod cbor_bytes;
66pub mod component;
67pub mod component_source;
68pub mod deployment;
69pub mod distributor;
70pub mod events;
71pub mod events_provider;
72pub mod flow;
73pub mod flow_resolve;
74pub mod flow_resolve_summary;
75pub mod i18n;
76pub mod messaging;
77pub mod op_descriptor;
78pub mod pack_manifest;
79pub mod provider;
80pub mod provider_install;
81pub mod qa;
82pub mod schema_id;
83pub mod store;
84pub mod supply_chain;
85pub mod worker;
86
87pub mod context;
88pub mod error;
89pub mod outcome;
90pub mod pack;
91pub mod policy;
92pub mod run;
93#[cfg(all(feature = "schemars", feature = "std"))]
94pub mod schema;
95pub mod secrets;
96pub mod session;
97pub mod state;
98pub mod telemetry;
99pub mod tenant;
100pub mod tenant_config;
101pub mod validate;
102
103pub use bindings::hints::{
104    BindingsHints, EnvHints, McpHints, McpServer, NetworkHints, SecretsHints,
105};
106pub use capabilities::{
107    Capabilities, FsCaps, HttpCaps, KvCaps, Limits, NetCaps, SecretsCaps, TelemetrySpec, ToolsCaps,
108};
109#[cfg(feature = "std")]
110pub use cbor::{CborError, decode_pack_manifest, encode_pack_manifest};
111pub use cbor_bytes::{Blob, CborBytes};
112pub use component::{
113    ComponentCapabilities, ComponentConfigurators, ComponentDevFlow, ComponentManifest,
114    ComponentOperation, ComponentProfileError, ComponentProfiles, EnvCapabilities,
115    EventsCapabilities, FilesystemCapabilities, FilesystemMode, FilesystemMount, HostCapabilities,
116    HttpCapabilities, IaCCapabilities, MessagingCapabilities, ResourceHints, SecretsCapabilities,
117    StateCapabilities, TelemetryCapabilities, TelemetryScope, WasiCapabilities,
118};
119pub use component_source::{ComponentSourceRef, ComponentSourceRefError};
120pub use context::{Cloud, DeploymentCtx, Platform};
121pub use deployment::{
122    ChannelPlan, DeploymentPlan, MessagingPlan, MessagingSubjectPlan, OAuthPlan, RunnerPlan,
123    TelemetryPlan,
124};
125pub use distributor::{
126    ArtifactLocation, CacheInfo, ComponentDigest, ComponentStatus, DistributorEnvironmentId,
127    PackStatusResponseV2, ResolveComponentRequest, ResolveComponentResponse, SignatureSummary,
128};
129pub use error::{ErrorCode, GResult, GreenticError};
130pub use events::{EventEnvelope, EventId, EventMetadata};
131pub use events_provider::{
132    EventProviderDescriptor, EventProviderKind, OrderingKind, ReliabilityKind, TransportKind,
133};
134pub use flow::{
135    ComponentRef as FlowComponentRef, Flow, FlowKind, FlowMetadata, InputMapping, Node,
136    OutputMapping, Routing, TelemetryHints,
137};
138pub use flow_resolve::{
139    ComponentSourceRefV1, FLOW_RESOLVE_SCHEMA_VERSION, FlowResolveV1, NodeResolveV1, ResolveModeV1,
140};
141#[cfg(all(feature = "std", feature = "serde"))]
142pub use flow_resolve::{read_flow_resolve, write_flow_resolve};
143#[cfg(feature = "std")]
144pub use flow_resolve::{sidecar_path_for_flow, validate_flow_resolve};
145pub use flow_resolve_summary::{
146    FLOW_RESOLVE_SUMMARY_SCHEMA_VERSION, FlowResolveSummaryManifestV1,
147    FlowResolveSummarySourceRefV1, FlowResolveSummaryV1, NodeResolveSummaryV1,
148};
149#[cfg(all(feature = "std", feature = "serde"))]
150pub use flow_resolve_summary::{read_flow_resolve_summary, write_flow_resolve_summary};
151#[cfg(feature = "std")]
152pub use flow_resolve_summary::{resolve_summary_path_for_flow, validate_flow_resolve_summary};
153pub use i18n::{Direction, I18nId, I18nTag, MinimalI18nProfile, id_for_tag};
154pub use messaging::{Actor, Attachment, ChannelMessageEnvelope, Destination, MessageMetadata};
155pub use op_descriptor::{IoSchema, OpDescriptor, OpExample};
156pub use outcome::Outcome;
157pub use pack::extensions::component_manifests::{
158    ComponentManifestIndexEntryV1, ComponentManifestIndexError, ComponentManifestIndexV1,
159    EXT_COMPONENT_MANIFEST_INDEX_V1, ManifestEncoding,
160};
161#[cfg(feature = "serde")]
162pub use pack::extensions::component_manifests::{
163    decode_component_manifest_index_v1_from_cbor_bytes,
164    encode_component_manifest_index_v1_to_cbor_bytes,
165};
166pub use pack::extensions::component_sources::{
167    ArtifactLocationV1, ComponentSourceEntryV1, ComponentSourcesError, ComponentSourcesV1,
168    EXT_COMPONENT_SOURCES_V1, ResolvedComponentV1,
169};
170#[cfg(feature = "serde")]
171pub use pack::extensions::component_sources::{
172    decode_component_sources_v1_from_cbor_bytes, encode_component_sources_v1_to_cbor_bytes,
173};
174pub use pack::{PackRef, Signature, SignatureAlgorithm};
175pub use pack_manifest::{
176    BootstrapSpec, ComponentCapability, ExtensionInline, ExtensionRef, PackDependency,
177    PackFlowEntry, PackKind, PackManifest, PackSignatures,
178};
179pub use policy::{AllowList, NetworkPolicy, PolicyDecision, PolicyDecisionStatus, Protocol};
180pub use provider::{
181    PROVIDER_EXTENSION_ID, ProviderDecl, ProviderExtensionInline, ProviderManifest,
182    ProviderRuntimeRef,
183};
184pub use provider_install::{ProviderInstallRecord, ProviderInstallRefs};
185pub use qa::{
186    CanonicalPolicy, ExampleAnswers, QaSpecSource, SetupContract, SetupOutput, validate_answers,
187};
188#[cfg(feature = "time")]
189pub use run::RunResult;
190pub use run::{NodeFailure, NodeStatus, NodeSummary, RunStatus, TranscriptOffset};
191pub use schema_id::{IoSchemaSource, QaSchemaSource, SchemaId, SchemaSource, schema_id_for_cbor};
192pub use secrets::{SecretFormat, SecretKey, SecretRequirement, SecretScope};
193pub use session::canonical_session_key;
194pub use session::{ReplyScope, SessionCursor, SessionData, SessionKey, WaitScope};
195pub use state::{StateKey, StatePath};
196pub use store::{
197    ArtifactSelector, BundleSpec, CapabilityMap, Collection, ConnectionKind, DesiredState,
198    DesiredStateExportSpec, DesiredSubscriptionEntry, Environment, LayoutSection,
199    LayoutSectionKind, PackOrComponentRef, PlanLimits, PriceModel, ProductOverride, RolloutState,
200    RolloutStatus, StoreFront, StorePlan, StoreProduct, StoreProductKind, Subscription,
201    SubscriptionStatus, Theme, VersionStrategy,
202};
203pub use supply_chain::{
204    AttestationStatement, BuildPlan, BuildStatus, BuildStatusKind, MetadataRecord, PredicateType,
205    RepoContext, ScanKind, ScanRequest, ScanResult, ScanStatusKind, SignRequest, StoreContext,
206    VerifyRequest, VerifyResult,
207};
208#[cfg(feature = "otel-keys")]
209pub use telemetry::OtlpKeys;
210pub use telemetry::SpanContext;
211#[cfg(feature = "telemetry-autoinit")]
212pub use telemetry::TelemetryCtx;
213pub use tenant::{Impersonation, TenantIdentity};
214pub use tenant_config::{
215    DefaultPipeline, DidContext, DidService, DistributorTarget, EnabledPacks,
216    IdentityProviderOption, RepoAuth, RepoConfigFeatures, RepoSkin, RepoSkinLayout, RepoSkinLinks,
217    RepoSkinTheme, RepoTenantConfig, RepoWorkerPanel, StoreTarget, TenantDidDocument,
218    VerificationMethod,
219};
220pub use validate::{
221    Diagnostic, PackValidator, Severity, ValidationCounts, ValidationReport,
222    validate_pack_manifest_core,
223};
224pub use worker::{WorkerMessage, WorkerRequest, WorkerResponse};
225
226#[cfg(feature = "schemars")]
227use alloc::borrow::Cow;
228use alloc::{borrow::ToOwned, collections::BTreeMap, format, string::String, vec::Vec};
229use core::fmt;
230use core::str::FromStr;
231#[cfg(feature = "schemars")]
232use schemars::JsonSchema;
233use semver::VersionReq;
234#[cfg(feature = "time")]
235use time::OffsetDateTime;
236
237#[cfg(feature = "serde")]
238use serde::{Deserialize, Serialize};
239
240#[cfg(feature = "std")]
241use alloc::boxed::Box;
242
243#[cfg(feature = "std")]
244use std::error::Error as StdError;
245
246/// Validates identifiers to ensure they are non-empty and ASCII-safe.
247pub(crate) fn validate_identifier(value: &str, label: &str) -> GResult<()> {
248    if value.is_empty() {
249        return Err(GreenticError::new(
250            ErrorCode::InvalidInput,
251            format!("{label} must not be empty"),
252        ));
253    }
254    if value
255        .chars()
256        .any(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
257    {
258        return Err(GreenticError::new(
259            ErrorCode::InvalidInput,
260            format!("{label} must contain only ASCII letters, digits, '.', '-', or '_'"),
261        ));
262    }
263    Ok(())
264}
265
266/// Validates API key references that may include URI-like prefixes.
267pub(crate) fn validate_api_key_ref(value: &str) -> GResult<()> {
268    if value.trim().is_empty() {
269        return Err(GreenticError::new(
270            ErrorCode::InvalidInput,
271            "ApiKeyRef must not be empty",
272        ));
273    }
274    if value.chars().any(char::is_whitespace) {
275        return Err(GreenticError::new(
276            ErrorCode::InvalidInput,
277            "ApiKeyRef must not contain whitespace",
278        ));
279    }
280    if !value.is_ascii() {
281        return Err(GreenticError::new(
282            ErrorCode::InvalidInput,
283            "ApiKeyRef must contain only ASCII characters",
284        ));
285    }
286    Ok(())
287}
288
289/// Canonical schema IDs for the exported document types.
290pub mod ids {
291    /// Pack identifier schema.
292    pub const PACK_ID: &str =
293        "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-id.schema.json";
294    /// Component identifier schema.
295    pub const COMPONENT_ID: &str =
296        "https://greentic-ai.github.io/greentic-types/schemas/v1/component-id.schema.json";
297    /// Flow identifier schema.
298    pub const FLOW_ID: &str =
299        "https://greentic-ai.github.io/greentic-types/schemas/v1/flow-id.schema.json";
300    /// Node identifier schema.
301    pub const NODE_ID: &str =
302        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-id.schema.json";
303    /// Tenant context schema.
304    pub const TENANT_CONTEXT: &str =
305        "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-context.schema.json";
306    /// Hash digest schema.
307    pub const HASH_DIGEST: &str =
308        "https://greentic-ai.github.io/greentic-types/schemas/v1/hash-digest.schema.json";
309    /// Semantic version requirement schema.
310    pub const SEMVER_REQ: &str =
311        "https://greentic-ai.github.io/greentic-types/schemas/v1/semver-req.schema.json";
312    /// Redaction path schema.
313    pub const REDACTION_PATH: &str =
314        "https://greentic-ai.github.io/greentic-types/schemas/v1/redaction-path.schema.json";
315    /// Capabilities schema.
316    pub const CAPABILITIES: &str =
317        "https://greentic-ai.github.io/greentic-types/schemas/v1/capabilities.schema.json";
318    /// RepoSkin (skin.json) schema.
319    pub const REPO_SKIN: &str =
320        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-skin.schema.json";
321    /// RepoAuth (auth.json) schema.
322    pub const REPO_AUTH: &str =
323        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-auth.schema.json";
324    /// RepoTenantConfig (config.json) schema.
325    pub const REPO_TENANT_CONFIG: &str =
326        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-tenant-config.schema.json";
327    /// Tenant DID document (did.json) schema.
328    pub const TENANT_DID_DOCUMENT: &str =
329        "https://greentic-ai.github.io/greentic-types/schemas/v1/tenant-did-document.schema.json";
330    /// Flow schema.
331    pub const FLOW: &str = "greentic.flow.v1";
332    /// Flow resolve sidecar schema.
333    pub const FLOW_RESOLVE: &str = "greentic.flow.resolve.v1";
334    /// Flow resolve summary schema.
335    pub const FLOW_RESOLVE_SUMMARY: &str = "greentic.flow.resolve-summary.v1";
336    /// Node schema.
337    pub const NODE: &str =
338        "https://greentic-ai.github.io/greentic-types/schemas/v1/node.schema.json";
339    /// Component manifest schema.
340    pub const COMPONENT_MANIFEST: &str =
341        "https://greentic-ai.github.io/greentic-types/schemas/v1/component-manifest.schema.json";
342    /// Pack manifest schema.
343    pub const PACK_MANIFEST: &str = "greentic.pack-manifest.v1";
344    /// Validation severity schema.
345    pub const VALIDATION_SEVERITY: &str =
346        "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-severity.schema.json";
347    /// Validation diagnostic schema.
348    pub const VALIDATION_DIAGNOSTIC: &str =
349        "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-diagnostic.schema.json";
350    /// Validation report schema.
351    pub const VALIDATION_REPORT: &str =
352        "https://greentic-ai.github.io/greentic-types/schemas/v1/validation-report.schema.json";
353    /// Provider manifest schema.
354    pub const PROVIDER_MANIFEST: &str =
355        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-manifest.schema.json";
356    /// Provider runtime reference schema.
357    pub const PROVIDER_RUNTIME_REF: &str =
358        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-runtime-ref.schema.json";
359    /// Provider declaration schema.
360    pub const PROVIDER_DECL: &str =
361        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-decl.schema.json";
362    /// Inline provider extension payload schema.
363    pub const PROVIDER_EXTENSION_INLINE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-extension-inline.schema.json";
364    /// Provider installation record schema.
365    pub const PROVIDER_INSTALL_RECORD: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-install-record.schema.json";
366    /// Limits schema.
367    pub const LIMITS: &str =
368        "https://greentic-ai.github.io/greentic-types/schemas/v1/limits.schema.json";
369    /// Telemetry spec schema.
370    pub const TELEMETRY_SPEC: &str =
371        "https://greentic-ai.github.io/greentic-types/schemas/v1/telemetry-spec.schema.json";
372    /// Node summary schema.
373    pub const NODE_SUMMARY: &str =
374        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-summary.schema.json";
375    /// Node failure schema.
376    pub const NODE_FAILURE: &str =
377        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-failure.schema.json";
378    /// Node status schema.
379    pub const NODE_STATUS: &str =
380        "https://greentic-ai.github.io/greentic-types/schemas/v1/node-status.schema.json";
381    /// Run status schema.
382    pub const RUN_STATUS: &str =
383        "https://greentic-ai.github.io/greentic-types/schemas/v1/run-status.schema.json";
384    /// Transcript offset schema.
385    pub const TRANSCRIPT_OFFSET: &str =
386        "https://greentic-ai.github.io/greentic-types/schemas/v1/transcript-offset.schema.json";
387    /// Tools capability schema.
388    pub const TOOLS_CAPS: &str =
389        "https://greentic-ai.github.io/greentic-types/schemas/v1/tools-caps.schema.json";
390    /// Secrets capability schema.
391    pub const SECRETS_CAPS: &str =
392        "https://greentic-ai.github.io/greentic-types/schemas/v1/secrets-caps.schema.json";
393    /// Branch reference schema.
394    pub const BRANCH_REF: &str =
395        "https://greentic-ai.github.io/greentic-types/schemas/v1/branch-ref.schema.json";
396    /// Commit reference schema.
397    pub const COMMIT_REF: &str =
398        "https://greentic-ai.github.io/greentic-types/schemas/v1/commit-ref.schema.json";
399    /// Git provider reference schema.
400    pub const GIT_PROVIDER_REF: &str =
401        "https://greentic-ai.github.io/greentic-types/schemas/v1/git-provider-ref.schema.json";
402    /// Scanner provider reference schema.
403    pub const SCANNER_REF: &str =
404        "https://greentic-ai.github.io/greentic-types/schemas/v1/scanner-ref.schema.json";
405    /// Webhook identifier schema.
406    pub const WEBHOOK_ID: &str =
407        "https://greentic-ai.github.io/greentic-types/schemas/v1/webhook-id.schema.json";
408    /// Provider installation identifier schema.
409    pub const PROVIDER_INSTALL_ID: &str =
410        "https://greentic-ai.github.io/greentic-types/schemas/v1/provider-install-id.schema.json";
411    /// Repository reference schema.
412    pub const REPO_REF: &str =
413        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-ref.schema.json";
414    /// Component reference schema.
415    pub const COMPONENT_REF: &str =
416        "https://greentic-ai.github.io/greentic-types/schemas/v1/component-ref.schema.json";
417    /// Version reference schema.
418    pub const VERSION_REF: &str =
419        "https://greentic-ai.github.io/greentic-types/schemas/v1/version-ref.schema.json";
420    /// Build reference schema.
421    pub const BUILD_REF: &str =
422        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-ref.schema.json";
423    /// Scan reference schema.
424    pub const SCAN_REF: &str =
425        "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-ref.schema.json";
426    /// Attestation reference schema.
427    pub const ATTESTATION_REF: &str =
428        "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-ref.schema.json";
429    /// Attestation id schema.
430    pub const ATTESTATION_ID: &str =
431        "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-id.schema.json";
432    /// Policy reference schema.
433    pub const POLICY_REF: &str =
434        "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-ref.schema.json";
435    /// Policy input reference schema.
436    pub const POLICY_INPUT_REF: &str =
437        "https://greentic-ai.github.io/greentic-types/schemas/v1/policy-input-ref.schema.json";
438    /// Store reference schema.
439    pub const STORE_REF: &str =
440        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-ref.schema.json";
441    /// Registry reference schema.
442    pub const REGISTRY_REF: &str =
443        "https://greentic-ai.github.io/greentic-types/schemas/v1/registry-ref.schema.json";
444    /// OCI image reference schema.
445    pub const OCI_IMAGE_REF: &str =
446        "https://greentic-ai.github.io/greentic-types/schemas/v1/oci-image-ref.schema.json";
447    /// Artifact reference schema.
448    pub const ARTIFACT_REF: &str =
449        "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-ref.schema.json";
450    /// SBOM reference schema.
451    pub const SBOM_REF: &str =
452        "https://greentic-ai.github.io/greentic-types/schemas/v1/sbom-ref.schema.json";
453    /// Signing key reference schema.
454    pub const SIGNING_KEY_REF: &str =
455        "https://greentic-ai.github.io/greentic-types/schemas/v1/signing-key-ref.schema.json";
456    /// Signature reference schema.
457    pub const SIGNATURE_REF: &str =
458        "https://greentic-ai.github.io/greentic-types/schemas/v1/signature-ref.schema.json";
459    /// Statement reference schema.
460    pub const STATEMENT_REF: &str =
461        "https://greentic-ai.github.io/greentic-types/schemas/v1/statement-ref.schema.json";
462    /// Build log reference schema.
463    pub const BUILD_LOG_REF: &str =
464        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-log-ref.schema.json";
465    /// Metadata record reference schema.
466    pub const METADATA_RECORD_REF: &str =
467        "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record-ref.schema.json";
468    /// API key reference schema.
469    pub const API_KEY_REF: &str =
470        "https://greentic-ai.github.io/greentic-types/schemas/v1/api-key-ref.schema.json";
471    /// Environment reference schema.
472    pub const ENVIRONMENT_REF: &str =
473        "https://greentic-ai.github.io/greentic-types/schemas/v1/environment-ref.schema.json";
474    /// Distributor reference schema.
475    pub const DISTRIBUTOR_REF: &str =
476        "https://greentic-ai.github.io/greentic-types/schemas/v1/distributor-ref.schema.json";
477    /// Storefront identifier schema.
478    pub const STOREFRONT_ID: &str =
479        "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront-id.schema.json";
480    /// Store product identifier schema.
481    pub const STORE_PRODUCT_ID: &str =
482        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-id.schema.json";
483    /// Store plan identifier schema.
484    pub const STORE_PLAN_ID: &str =
485        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan-id.schema.json";
486    /// Subscription identifier schema.
487    pub const SUBSCRIPTION_ID: &str =
488        "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-id.schema.json";
489    /// Bundle identifier schema.
490    pub const BUNDLE_ID: &str =
491        "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle-id.schema.json";
492    /// Collection identifier schema.
493    pub const COLLECTION_ID: &str =
494        "https://greentic-ai.github.io/greentic-types/schemas/v1/collection-id.schema.json";
495    /// Artifact selector schema.
496    pub const ARTIFACT_SELECTOR: &str =
497        "https://greentic-ai.github.io/greentic-types/schemas/v1/artifact-selector.schema.json";
498    /// Capability map schema.
499    pub const CAPABILITY_MAP: &str =
500        "https://greentic-ai.github.io/greentic-types/schemas/v1/capability-map.schema.json";
501    /// Store product kind schema.
502    pub const STORE_PRODUCT_KIND: &str =
503        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product-kind.schema.json";
504    /// Version strategy schema.
505    pub const VERSION_STRATEGY: &str =
506        "https://greentic-ai.github.io/greentic-types/schemas/v1/version-strategy.schema.json";
507    /// Rollout status schema.
508    pub const ROLLOUT_STATUS: &str =
509        "https://greentic-ai.github.io/greentic-types/schemas/v1/rollout-status.schema.json";
510    /// Connection kind schema.
511    pub const CONNECTION_KIND: &str =
512        "https://greentic-ai.github.io/greentic-types/schemas/v1/connection-kind.schema.json";
513    /// Pack or component reference schema.
514    pub const PACK_OR_COMPONENT_REF: &str =
515        "https://greentic-ai.github.io/greentic-types/schemas/v1/pack-or-component-ref.schema.json";
516    /// Plan limits schema.
517    pub const PLAN_LIMITS: &str =
518        "https://greentic-ai.github.io/greentic-types/schemas/v1/plan-limits.schema.json";
519    /// Price model schema.
520    pub const PRICE_MODEL: &str =
521        "https://greentic-ai.github.io/greentic-types/schemas/v1/price-model.schema.json";
522    /// Subscription status schema.
523    pub const SUBSCRIPTION_STATUS: &str =
524        "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription-status.schema.json";
525    /// Build plan schema.
526    pub const BUILD_PLAN: &str =
527        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-plan.schema.json";
528    /// Build status schema.
529    pub const BUILD_STATUS: &str =
530        "https://greentic-ai.github.io/greentic-types/schemas/v1/build-status.schema.json";
531    /// Scan request schema.
532    pub const SCAN_REQUEST: &str =
533        "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-request.schema.json";
534    /// Scan result schema.
535    pub const SCAN_RESULT: &str =
536        "https://greentic-ai.github.io/greentic-types/schemas/v1/scan-result.schema.json";
537    /// Sign request schema.
538    pub const SIGN_REQUEST: &str =
539        "https://greentic-ai.github.io/greentic-types/schemas/v1/sign-request.schema.json";
540    /// Verify request schema.
541    pub const VERIFY_REQUEST: &str =
542        "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-request.schema.json";
543    /// Verify result schema.
544    pub const VERIFY_RESULT: &str =
545        "https://greentic-ai.github.io/greentic-types/schemas/v1/verify-result.schema.json";
546    /// Attestation statement schema.
547    pub const ATTESTATION_STATEMENT: &str =
548        "https://greentic-ai.github.io/greentic-types/schemas/v1/attestation-statement.schema.json";
549    /// Metadata record schema.
550    pub const METADATA_RECORD: &str =
551        "https://greentic-ai.github.io/greentic-types/schemas/v1/metadata-record.schema.json";
552    /// Repository context schema.
553    pub const REPO_CONTEXT: &str =
554        "https://greentic-ai.github.io/greentic-types/schemas/v1/repo-context.schema.json";
555    /// Store context schema.
556    pub const STORE_CONTEXT: &str =
557        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-context.schema.json";
558    /// Bundle schema.
559    pub const BUNDLE: &str =
560        "https://greentic-ai.github.io/greentic-types/schemas/v1/bundle.schema.json";
561    /// Bundle export specification schema.
562    pub const DESIRED_STATE_EXPORT: &str =
563        "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state-export.schema.json";
564    /// Desired state schema.
565    pub const DESIRED_STATE: &str =
566        "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-state.schema.json";
567    /// Desired subscription entry schema.
568    pub const DESIRED_SUBSCRIPTION_ENTRY: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/desired-subscription-entry.schema.json";
569    /// Storefront schema.
570    pub const STOREFRONT: &str =
571        "https://greentic-ai.github.io/greentic-types/schemas/v1/storefront.schema.json";
572    /// Store product schema.
573    pub const STORE_PRODUCT: &str =
574        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-product.schema.json";
575    /// Store plan schema.
576    pub const STORE_PLAN: &str =
577        "https://greentic-ai.github.io/greentic-types/schemas/v1/store-plan.schema.json";
578    /// Subscription schema.
579    pub const SUBSCRIPTION: &str =
580        "https://greentic-ai.github.io/greentic-types/schemas/v1/subscription.schema.json";
581    /// Environment schema.
582    pub const ENVIRONMENT: &str =
583        "https://greentic-ai.github.io/greentic-types/schemas/v1/environment.schema.json";
584    /// Store theme schema.
585    pub const THEME: &str =
586        "https://greentic-ai.github.io/greentic-types/schemas/v1/theme.schema.json";
587    /// Layout section schema.
588    pub const LAYOUT_SECTION: &str =
589        "https://greentic-ai.github.io/greentic-types/schemas/v1/layout-section.schema.json";
590    /// Collection schema.
591    pub const COLLECTION: &str =
592        "https://greentic-ai.github.io/greentic-types/schemas/v1/collection.schema.json";
593    /// Product override schema.
594    pub const PRODUCT_OVERRIDE: &str =
595        "https://greentic-ai.github.io/greentic-types/schemas/v1/product-override.schema.json";
596    /// Event envelope schema.
597    pub const EVENT_ENVELOPE: &str =
598        "https://greentic-ai.github.io/greentic-types/schemas/v1/event-envelope.schema.json";
599    /// Event provider descriptor schema.
600    pub const EVENT_PROVIDER_DESCRIPTOR: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/event-provider-descriptor.schema.json";
601    /// Channel message envelope schema.
602    pub const CHANNEL_MESSAGE_ENVELOPE: &str = "https://greentic-ai.github.io/greentic-types/schemas/v1/channel-message-envelope.schema.json";
603    /// Attachment schema.
604    pub const ATTACHMENT: &str =
605        "https://greentic-ai.github.io/greentic-types/schemas/v1/attachment.schema.json";
606    /// Worker request envelope schema.
607    pub const WORKER_REQUEST: &str =
608        "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-request.schema.json";
609    /// Worker message schema.
610    pub const WORKER_MESSAGE: &str =
611        "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-message.schema.json";
612    /// Worker response envelope schema.
613    pub const WORKER_RESPONSE: &str =
614        "https://greentic-ai.github.io/greentic-types/schemas/v1/worker-response.schema.json";
615    /// OTLP attribute key schema.
616    pub const OTLP_KEYS: &str =
617        "https://greentic-ai.github.io/greentic-types/schemas/v1/otlp-keys.schema.json";
618    /// Run result schema.
619    pub const RUN_RESULT: &str =
620        "https://greentic-ai.github.io/greentic-types/schemas/v1/run-result.schema.json";
621}
622
623#[cfg(all(feature = "schema", feature = "std"))]
624/// Writes every JSON Schema to the provided directory.
625pub fn write_all_schemas(out_dir: &std::path::Path) -> anyhow::Result<()> {
626    use anyhow::Context;
627    use std::fs;
628
629    fs::create_dir_all(out_dir)
630        .with_context(|| format!("failed to create {}", out_dir.display()))?;
631
632    for entry in crate::schema::entries() {
633        let schema = (entry.generator)();
634        let path = out_dir.join(entry.file_name);
635        if let Some(parent) = path.parent() {
636            fs::create_dir_all(parent)
637                .with_context(|| format!("failed to create {}", parent.display()))?;
638        }
639
640        let json =
641            serde_json::to_vec_pretty(&schema).context("failed to serialize schema to JSON")?;
642        fs::write(&path, json).with_context(|| format!("failed to write {}", path.display()))?;
643    }
644
645    Ok(())
646}
647
648macro_rules! id_newtype {
649    ($name:ident, $doc:literal) => {
650        #[doc = $doc]
651        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
652        #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
653        #[cfg_attr(feature = "schemars", derive(JsonSchema))]
654        #[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
655        pub struct $name(pub String);
656
657        impl $name {
658            /// Returns the identifier as a string slice.
659            pub fn as_str(&self) -> &str {
660                &self.0
661            }
662
663            /// Validates and constructs the identifier from the provided value.
664            pub fn new(value: impl AsRef<str>) -> GResult<Self> {
665                value.as_ref().parse()
666            }
667        }
668
669        impl From<$name> for String {
670            fn from(value: $name) -> Self {
671                value.0
672            }
673        }
674
675        impl AsRef<str> for $name {
676            fn as_ref(&self) -> &str {
677                self.as_str()
678            }
679        }
680
681        impl fmt::Display for $name {
682            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
683                f.write_str(self.as_str())
684            }
685        }
686
687        impl FromStr for $name {
688            type Err = GreenticError;
689
690            fn from_str(value: &str) -> Result<Self, Self::Err> {
691                validate_identifier(value, stringify!($name))?;
692                Ok(Self(value.to_owned()))
693            }
694        }
695
696        impl TryFrom<String> for $name {
697            type Error = GreenticError;
698
699            fn try_from(value: String) -> Result<Self, Self::Error> {
700                $name::from_str(&value)
701            }
702        }
703
704        impl TryFrom<&str> for $name {
705            type Error = GreenticError;
706
707            fn try_from(value: &str) -> Result<Self, Self::Error> {
708                $name::from_str(value)
709            }
710        }
711    };
712}
713
714id_newtype!(EnvId, "Environment identifier for a tenant context.");
715id_newtype!(TenantId, "Tenant identifier within an environment.");
716id_newtype!(TeamId, "Team identifier belonging to a tenant.");
717id_newtype!(UserId, "User identifier within a tenant.");
718id_newtype!(BranchRef, "Reference to a source control branch.");
719id_newtype!(CommitRef, "Reference to a source control commit.");
720id_newtype!(
721    GitProviderRef,
722    "Identifier referencing a source control provider."
723);
724id_newtype!(ScannerRef, "Identifier referencing a scanner provider.");
725id_newtype!(WebhookId, "Identifier referencing a registered webhook.");
726id_newtype!(
727    ProviderInstallId,
728    "Identifier referencing a provider installation record."
729);
730id_newtype!(PackId, "Globally unique pack identifier.");
731id_newtype!(
732    ComponentId,
733    "Identifier referencing a component binding in a pack."
734);
735id_newtype!(FlowId, "Identifier referencing a flow inside a pack.");
736id_newtype!(NodeId, "Identifier referencing a node inside a flow graph.");
737id_newtype!(
738    EnvironmentRef,
739    "Identifier referencing a deployment environment."
740);
741id_newtype!(
742    DistributorRef,
743    "Identifier referencing a distributor instance."
744);
745id_newtype!(StoreFrontId, "Identifier referencing a storefront.");
746id_newtype!(
747    StoreProductId,
748    "Identifier referencing a product in the store catalog."
749);
750id_newtype!(
751    StorePlanId,
752    "Identifier referencing a plan for a store product."
753);
754id_newtype!(
755    SubscriptionId,
756    "Identifier referencing a subscription entry."
757);
758id_newtype!(BundleId, "Identifier referencing a distributor bundle.");
759id_newtype!(CollectionId, "Identifier referencing a product collection.");
760id_newtype!(RepoRef, "Repository reference within a supply chain.");
761id_newtype!(
762    ComponentRef,
763    "Supply-chain component reference (distinct from pack ComponentId)."
764);
765id_newtype!(
766    VersionRef,
767    "Version reference for a component or metadata record."
768);
769id_newtype!(BuildRef, "Build reference within a supply chain.");
770id_newtype!(ScanRef, "Scan reference within a supply chain.");
771id_newtype!(
772    AttestationRef,
773    "Attestation reference within a supply chain."
774);
775id_newtype!(AttestationId, "Identifier referencing an attestation.");
776id_newtype!(PolicyRef, "Policy reference within a supply chain.");
777id_newtype!(
778    PolicyInputRef,
779    "Reference to a policy input payload for evaluation."
780);
781id_newtype!(StoreRef, "Content store reference within a supply chain.");
782id_newtype!(
783    RegistryRef,
784    "Registry reference for OCI or artifact storage."
785);
786id_newtype!(
787    OciImageRef,
788    "Reference to an OCI image for distribution (oci://repo/name:tag or oci://repo/name@sha256:...)."
789);
790id_newtype!(
791    ArtifactRef,
792    "Artifact reference within a build or scan result."
793);
794id_newtype!(
795    SbomRef,
796    "Reference to a Software Bill of Materials artifact."
797);
798id_newtype!(SigningKeyRef, "Reference to a signing key handle.");
799id_newtype!(SignatureRef, "Reference to a generated signature.");
800id_newtype!(StatementRef, "Reference to an attestation statement.");
801id_newtype!(
802    BuildLogRef,
803    "Reference to a build log output produced during execution."
804);
805id_newtype!(
806    MetadataRecordRef,
807    "Reference to a metadata record attached to artifacts or bundles."
808);
809
810/// API key reference used across secrets providers without exposing key material.
811#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
812#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
813#[cfg_attr(feature = "schemars", derive(JsonSchema))]
814#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
815pub struct ApiKeyRef(pub String);
816
817impl ApiKeyRef {
818    /// Returns the reference as a string slice.
819    pub fn as_str(&self) -> &str {
820        &self.0
821    }
822
823    /// Validates and constructs the reference from the provided value.
824    pub fn new(value: impl AsRef<str>) -> GResult<Self> {
825        value.as_ref().parse()
826    }
827}
828
829impl fmt::Display for ApiKeyRef {
830    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
831        f.write_str(self.as_str())
832    }
833}
834
835impl FromStr for ApiKeyRef {
836    type Err = GreenticError;
837
838    fn from_str(value: &str) -> Result<Self, Self::Err> {
839        validate_api_key_ref(value)?;
840        Ok(Self(value.to_owned()))
841    }
842}
843
844impl TryFrom<String> for ApiKeyRef {
845    type Error = GreenticError;
846
847    fn try_from(value: String) -> Result<Self, Self::Error> {
848        ApiKeyRef::from_str(&value)
849    }
850}
851
852impl TryFrom<&str> for ApiKeyRef {
853    type Error = GreenticError;
854
855    fn try_from(value: &str) -> Result<Self, Self::Error> {
856        ApiKeyRef::from_str(value)
857    }
858}
859
860impl From<ApiKeyRef> for String {
861    fn from(value: ApiKeyRef) -> Self {
862        value.0
863    }
864}
865
866impl AsRef<str> for ApiKeyRef {
867    fn as_ref(&self) -> &str {
868        self.as_str()
869    }
870}
871
872/// Compact tenant summary propagated to developers and tooling.
873#[derive(Clone, Debug, PartialEq, Eq, Hash)]
874#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
875#[cfg_attr(feature = "schemars", derive(JsonSchema))]
876pub struct TenantContext {
877    /// Tenant identifier owning the execution.
878    pub tenant_id: TenantId,
879    /// Optional team identifier scoped to the tenant.
880    #[cfg_attr(
881        feature = "serde",
882        serde(default, skip_serializing_if = "Option::is_none")
883    )]
884    pub team_id: Option<TeamId>,
885    /// Optional user identifier scoped to the tenant.
886    #[cfg_attr(
887        feature = "serde",
888        serde(default, skip_serializing_if = "Option::is_none")
889    )]
890    pub user_id: Option<UserId>,
891    /// Optional session identifier for end-to-end correlation.
892    #[cfg_attr(
893        feature = "serde",
894        serde(default, skip_serializing_if = "Option::is_none")
895    )]
896    pub session_id: Option<String>,
897    /// Optional attributes for routing and tracing.
898    #[cfg_attr(
899        feature = "serde",
900        serde(default, skip_serializing_if = "BTreeMap::is_empty")
901    )]
902    pub attributes: BTreeMap<String, String>,
903}
904
905impl TenantContext {
906    /// Creates a new tenant context scoped to the provided tenant id.
907    pub fn new(tenant_id: TenantId) -> Self {
908        Self {
909            tenant_id,
910            team_id: None,
911            user_id: None,
912            session_id: None,
913            attributes: BTreeMap::new(),
914        }
915    }
916}
917
918impl From<&TenantCtx> for TenantContext {
919    fn from(ctx: &TenantCtx) -> Self {
920        Self {
921            tenant_id: ctx.tenant_id.clone(),
922            team_id: ctx.team_id.clone().or_else(|| ctx.team.clone()),
923            user_id: ctx.user_id.clone().or_else(|| ctx.user.clone()),
924            session_id: ctx.session_id.clone(),
925            attributes: ctx.attributes.clone(),
926        }
927    }
928}
929
930/// Supported hashing algorithms for pack content digests.
931#[derive(Clone, Debug, PartialEq, Eq, Hash)]
932#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
933#[cfg_attr(feature = "schemars", derive(JsonSchema))]
934#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
935pub enum HashAlgorithm {
936    /// Blake3 hashing algorithm.
937    Blake3,
938    /// Catch all for other algorithms.
939    Other(String),
940}
941
942/// Content digest describing a pack or artifact.
943#[derive(Clone, Debug, PartialEq, Eq, Hash)]
944#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
945#[cfg_attr(
946    feature = "serde",
947    serde(into = "HashDigestRepr", try_from = "HashDigestRepr")
948)]
949#[cfg_attr(feature = "schemars", derive(JsonSchema))]
950pub struct HashDigest {
951    /// Hash algorithm used to produce the digest.
952    pub algo: HashAlgorithm,
953    /// Hex encoded digest bytes.
954    pub hex: String,
955}
956
957impl HashDigest {
958    /// Creates a new digest ensuring the hex payload is valid.
959    pub fn new(algo: HashAlgorithm, hex: impl Into<String>) -> GResult<Self> {
960        let hex = hex.into();
961        validate_hex(&hex)?;
962        Ok(Self { algo, hex })
963    }
964
965    /// Convenience constructor for Blake3 digests.
966    pub fn blake3(hex: impl Into<String>) -> GResult<Self> {
967        Self::new(HashAlgorithm::Blake3, hex)
968    }
969}
970
971#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
972#[cfg_attr(feature = "schemars", derive(JsonSchema))]
973struct HashDigestRepr {
974    algo: HashAlgorithm,
975    hex: String,
976}
977
978impl From<HashDigest> for HashDigestRepr {
979    fn from(value: HashDigest) -> Self {
980        Self {
981            algo: value.algo,
982            hex: value.hex,
983        }
984    }
985}
986
987impl TryFrom<HashDigestRepr> for HashDigest {
988    type Error = GreenticError;
989
990    fn try_from(value: HashDigestRepr) -> Result<Self, Self::Error> {
991        HashDigest::new(value.algo, value.hex)
992    }
993}
994
995fn validate_hex(hex: &str) -> GResult<()> {
996    if hex.is_empty() {
997        return Err(GreenticError::new(
998            ErrorCode::InvalidInput,
999            "digest hex payload must not be empty",
1000        ));
1001    }
1002    if hex.len() % 2 != 0 {
1003        return Err(GreenticError::new(
1004            ErrorCode::InvalidInput,
1005            "digest hex payload must have an even number of digits",
1006        ));
1007    }
1008    if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
1009        return Err(GreenticError::new(
1010            ErrorCode::InvalidInput,
1011            "digest hex payload must be hexadecimal",
1012        ));
1013    }
1014    Ok(())
1015}
1016
1017/// Semantic version requirement validated by [`semver`].
1018#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1019#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1020#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1021pub struct SemverReq(String);
1022
1023impl SemverReq {
1024    /// Parses and validates a semantic version requirement string.
1025    pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1026        let value = value.as_ref();
1027        VersionReq::parse(value).map_err(|err| {
1028            GreenticError::new(
1029                ErrorCode::InvalidInput,
1030                format!("invalid semver requirement '{value}': {err}"),
1031            )
1032        })?;
1033        Ok(Self(value.to_owned()))
1034    }
1035
1036    /// Returns the underlying string slice.
1037    pub fn as_str(&self) -> &str {
1038        &self.0
1039    }
1040
1041    /// Converts into a [`semver::VersionReq`].
1042    pub fn to_version_req(&self) -> VersionReq {
1043        VersionReq::parse(&self.0)
1044            .unwrap_or_else(|err| unreachable!("SemverReq::parse validated inputs: {err}"))
1045    }
1046}
1047
1048impl fmt::Display for SemverReq {
1049    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1050        f.write_str(self.as_str())
1051    }
1052}
1053
1054impl From<SemverReq> for String {
1055    fn from(value: SemverReq) -> Self {
1056        value.0
1057    }
1058}
1059
1060impl TryFrom<String> for SemverReq {
1061    type Error = GreenticError;
1062
1063    fn try_from(value: String) -> Result<Self, Self::Error> {
1064        SemverReq::parse(&value)
1065    }
1066}
1067
1068impl TryFrom<&str> for SemverReq {
1069    type Error = GreenticError;
1070
1071    fn try_from(value: &str) -> Result<Self, Self::Error> {
1072        SemverReq::parse(value)
1073    }
1074}
1075
1076impl FromStr for SemverReq {
1077    type Err = GreenticError;
1078
1079    fn from_str(s: &str) -> Result<Self, Self::Err> {
1080        SemverReq::parse(s)
1081    }
1082}
1083
1084/// JSONPath expression pointing at sensitive fields that should be redacted.
1085#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1086#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1087#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))]
1088pub struct RedactionPath(String);
1089
1090impl RedactionPath {
1091    /// Validates and stores a JSONPath expression.
1092    pub fn parse(value: impl AsRef<str>) -> GResult<Self> {
1093        let value = value.as_ref();
1094        validate_jsonpath(value)?;
1095        Ok(Self(value.to_owned()))
1096    }
1097
1098    /// Returns the JSONPath string.
1099    pub fn as_str(&self) -> &str {
1100        &self.0
1101    }
1102}
1103
1104impl fmt::Display for RedactionPath {
1105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1106        f.write_str(self.as_str())
1107    }
1108}
1109
1110impl From<RedactionPath> for String {
1111    fn from(value: RedactionPath) -> Self {
1112        value.0
1113    }
1114}
1115
1116impl TryFrom<String> for RedactionPath {
1117    type Error = GreenticError;
1118
1119    fn try_from(value: String) -> Result<Self, Self::Error> {
1120        RedactionPath::parse(&value)
1121    }
1122}
1123
1124impl TryFrom<&str> for RedactionPath {
1125    type Error = GreenticError;
1126
1127    fn try_from(value: &str) -> Result<Self, Self::Error> {
1128        RedactionPath::parse(value)
1129    }
1130}
1131
1132fn validate_jsonpath(path: &str) -> GResult<()> {
1133    if path.is_empty() {
1134        return Err(GreenticError::new(
1135            ErrorCode::InvalidInput,
1136            "redaction path cannot be empty",
1137        ));
1138    }
1139    if !path.starts_with('$') {
1140        return Err(GreenticError::new(
1141            ErrorCode::InvalidInput,
1142            "redaction path must start with '$'",
1143        ));
1144    }
1145    if path.chars().any(|c| c.is_control()) {
1146        return Err(GreenticError::new(
1147            ErrorCode::InvalidInput,
1148            "redaction path cannot contain control characters",
1149        ));
1150    }
1151    Ok(())
1152}
1153
1154#[cfg(feature = "schemars")]
1155impl JsonSchema for SemverReq {
1156    fn schema_name() -> Cow<'static, str> {
1157        Cow::Borrowed("SemverReq")
1158    }
1159
1160    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1161        let mut schema = <String>::json_schema(generator);
1162        if schema.get("description").is_none() {
1163            schema.insert(
1164                "description".into(),
1165                "Validated semantic version requirement string".into(),
1166            );
1167        }
1168        schema
1169    }
1170}
1171
1172#[cfg(feature = "schemars")]
1173impl JsonSchema for RedactionPath {
1174    fn schema_name() -> Cow<'static, str> {
1175        Cow::Borrowed("RedactionPath")
1176    }
1177
1178    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1179        let mut schema = <String>::json_schema(generator);
1180        if schema.get("description").is_none() {
1181            schema.insert(
1182                "description".into(),
1183                "JSONPath expression used for runtime redaction".into(),
1184            );
1185        }
1186        schema
1187    }
1188}
1189
1190/// Deadline metadata for an invocation, stored as Unix epoch milliseconds.
1191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1192#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1193#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1194pub struct InvocationDeadline {
1195    unix_millis: i128,
1196}
1197
1198impl InvocationDeadline {
1199    /// Creates a deadline from a Unix timestamp expressed in milliseconds.
1200    pub const fn from_unix_millis(unix_millis: i128) -> Self {
1201        Self { unix_millis }
1202    }
1203
1204    /// Returns the deadline as Unix epoch milliseconds.
1205    pub const fn unix_millis(&self) -> i128 {
1206        self.unix_millis
1207    }
1208
1209    /// Converts the deadline into an [`OffsetDateTime`].
1210    #[cfg(feature = "time")]
1211    pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, time::error::ComponentRange> {
1212        OffsetDateTime::from_unix_timestamp_nanos(self.unix_millis * 1_000_000)
1213    }
1214
1215    /// Creates a deadline from an [`OffsetDateTime`], truncating to milliseconds.
1216    #[cfg(feature = "time")]
1217    pub fn from_offset_date_time(value: OffsetDateTime) -> Self {
1218        let nanos = value.unix_timestamp_nanos();
1219        Self {
1220            unix_millis: nanos / 1_000_000,
1221        }
1222    }
1223}
1224
1225/// Context that accompanies every invocation across Greentic runtimes.
1226#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1228#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1229pub struct TenantCtx {
1230    /// Environment scope (for example `dev`, `staging`, or `prod`).
1231    pub env: EnvId,
1232    /// Tenant identifier for the current execution.
1233    pub tenant: TenantId,
1234    /// Stable tenant identifier reference used across systems.
1235    pub tenant_id: TenantId,
1236    /// Optional team identifier scoped to the tenant.
1237    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1238    pub team: Option<TeamId>,
1239    /// Optional team identifier accessible via the shared schema.
1240    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1241    pub team_id: Option<TeamId>,
1242    /// Optional user identifier scoped to the tenant.
1243    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1244    pub user: Option<UserId>,
1245    /// Optional user identifier aligned with the shared schema.
1246    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1247    pub user_id: Option<UserId>,
1248    /// Optional session identifier propagated by the runtime.
1249    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1250    pub session_id: Option<String>,
1251    /// Optional flow identifier for the current execution.
1252    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1253    pub flow_id: Option<String>,
1254    /// Optional node identifier within the flow.
1255    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1256    pub node_id: Option<String>,
1257    /// Optional provider identifier describing the runtime surface.
1258    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1259    pub provider_id: Option<String>,
1260    /// Distributed tracing identifier when available.
1261    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1262    pub trace_id: Option<String>,
1263    /// Optional locale/translation identifier for the session.
1264    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1265    pub i18n_id: Option<String>,
1266    /// Correlation identifier for linking related events.
1267    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1268    pub correlation_id: Option<String>,
1269    /// Free-form attributes for routing and tracing.
1270    #[cfg_attr(
1271        feature = "serde",
1272        serde(default, skip_serializing_if = "BTreeMap::is_empty")
1273    )]
1274    pub attributes: BTreeMap<String, String>,
1275    /// Deadline when the invocation should finish.
1276    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1277    pub deadline: Option<InvocationDeadline>,
1278    /// Attempt counter for retried invocations (starting at zero).
1279    pub attempt: u32,
1280    /// Stable idempotency key propagated across retries.
1281    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1282    pub idempotency_key: Option<String>,
1283    /// Optional impersonation context describing the acting identity.
1284    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1285    pub impersonation: Option<Impersonation>,
1286}
1287
1288impl TenantCtx {
1289    /// Creates a new tenant context with the provided environment and tenant identifiers.
1290    pub fn new(env: EnvId, tenant: TenantId) -> Self {
1291        let tenant_id = tenant.clone();
1292        Self {
1293            env,
1294            tenant: tenant.clone(),
1295            tenant_id,
1296            team: None,
1297            team_id: None,
1298            user: None,
1299            user_id: None,
1300            session_id: None,
1301            flow_id: None,
1302            node_id: None,
1303            provider_id: None,
1304            trace_id: None,
1305            i18n_id: None,
1306            correlation_id: None,
1307            attributes: BTreeMap::new(),
1308            deadline: None,
1309            attempt: 0,
1310            idempotency_key: None,
1311            impersonation: None,
1312        }
1313    }
1314
1315    /// Updates the team information ensuring legacy and shared fields stay aligned.
1316    pub fn with_team(mut self, team: Option<TeamId>) -> Self {
1317        self.team = team.clone();
1318        self.team_id = team;
1319        self
1320    }
1321
1322    /// Updates the user information ensuring legacy and shared fields stay aligned.
1323    pub fn with_user(mut self, user: Option<UserId>) -> Self {
1324        self.user = user.clone();
1325        self.user_id = user;
1326        self
1327    }
1328
1329    /// Updates the session identifier.
1330    pub fn with_session(mut self, session: impl Into<String>) -> Self {
1331        self.session_id = Some(session.into());
1332        self
1333    }
1334
1335    /// Updates the flow identifier.
1336    pub fn with_flow(mut self, flow: impl Into<String>) -> Self {
1337        self.flow_id = Some(flow.into());
1338        self
1339    }
1340
1341    /// Updates the node identifier.
1342    pub fn with_node(mut self, node: impl Into<String>) -> Self {
1343        self.node_id = Some(node.into());
1344        self
1345    }
1346
1347    /// Updates the provider identifier.
1348    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1349        self.provider_id = Some(provider.into());
1350        self
1351    }
1352
1353    /// Attaches or replaces the attributes map.
1354    pub fn with_attributes(mut self, attributes: BTreeMap<String, String>) -> Self {
1355        self.attributes = attributes;
1356        self
1357    }
1358
1359    /// Sets the impersonation context.
1360    pub fn with_impersonation(mut self, impersonation: Option<Impersonation>) -> Self {
1361        self.impersonation = impersonation;
1362        self
1363    }
1364
1365    /// Returns a copy of the context with the provided attempt value.
1366    pub fn with_attempt(mut self, attempt: u32) -> Self {
1367        self.attempt = attempt;
1368        self
1369    }
1370
1371    /// Updates the deadline metadata for subsequent invocations.
1372    pub fn with_deadline(mut self, deadline: Option<InvocationDeadline>) -> Self {
1373        self.deadline = deadline;
1374        self
1375    }
1376
1377    /// Returns the session identifier, when present.
1378    pub fn session_id(&self) -> Option<&str> {
1379        self.session_id.as_deref()
1380    }
1381
1382    /// Returns the flow identifier, when present.
1383    pub fn flow_id(&self) -> Option<&str> {
1384        self.flow_id.as_deref()
1385    }
1386
1387    /// Returns the node identifier, when present.
1388    pub fn node_id(&self) -> Option<&str> {
1389        self.node_id.as_deref()
1390    }
1391
1392    /// Returns the provider identifier, when present.
1393    pub fn provider_id(&self) -> Option<&str> {
1394        self.provider_id.as_deref()
1395    }
1396}
1397
1398/// Primary payload representation shared across envelopes.
1399pub type BinaryPayload = Vec<u8>;
1400
1401/// Normalized ingress payload delivered to nodes.
1402#[derive(Clone, Debug, PartialEq, Eq)]
1403#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1404#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1405pub struct InvocationEnvelope {
1406    /// Tenant context for the invocation.
1407    pub ctx: TenantCtx,
1408    /// Flow identifier the event belongs to.
1409    pub flow_id: String,
1410    /// Optional node identifier within the flow.
1411    pub node_id: Option<String>,
1412    /// Operation being invoked (for example `on_message` or `tick`).
1413    pub op: String,
1414    /// Normalized payload for the invocation.
1415    pub payload: BinaryPayload,
1416    /// Raw metadata propagated from the ingress surface.
1417    pub metadata: BinaryPayload,
1418}
1419
1420/// Structured detail payload attached to a node error.
1421#[derive(Clone, Debug, PartialEq, Eq)]
1422#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1423#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1424pub enum ErrorDetail {
1425    /// UTF-8 encoded detail payload.
1426    Text(String),
1427    /// Binary payload detail (for example message pack or CBOR).
1428    Binary(BinaryPayload),
1429}
1430
1431/// Error type emitted by Greentic nodes.
1432#[derive(Debug)]
1433#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1434#[cfg_attr(feature = "schemars", derive(JsonSchema))]
1435pub struct NodeError {
1436    /// Machine readable error code.
1437    pub code: String,
1438    /// Human readable message explaining the failure.
1439    pub message: String,
1440    /// Whether the failure is retryable by the runtime.
1441    pub retryable: bool,
1442    /// Optional backoff duration in milliseconds for the next retry.
1443    pub backoff_ms: Option<u64>,
1444    /// Optional structured error detail payload.
1445    pub details: Option<ErrorDetail>,
1446    #[cfg(feature = "std")]
1447    #[cfg_attr(feature = "serde", serde(skip, default = "default_source"))]
1448    #[cfg_attr(feature = "schemars", schemars(skip))]
1449    source: Option<Box<dyn StdError + Send + Sync>>,
1450}
1451
1452#[cfg(all(feature = "std", feature = "serde"))]
1453fn default_source() -> Option<Box<dyn StdError + Send + Sync>> {
1454    None
1455}
1456
1457impl NodeError {
1458    /// Constructs a non-retryable failure with the supplied code and message.
1459    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
1460        Self {
1461            code: code.into(),
1462            message: message.into(),
1463            retryable: false,
1464            backoff_ms: None,
1465            details: None,
1466            #[cfg(feature = "std")]
1467            source: None,
1468        }
1469    }
1470
1471    /// Marks the error as retryable with an optional backoff value.
1472    pub fn with_retry(mut self, backoff_ms: Option<u64>) -> Self {
1473        self.retryable = true;
1474        self.backoff_ms = backoff_ms;
1475        self
1476    }
1477
1478    /// Attaches structured details to the error.
1479    pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
1480        self.details = Some(detail);
1481        self
1482    }
1483
1484    /// Attaches a textual detail payload to the error.
1485    pub fn with_detail_text(mut self, detail: impl Into<String>) -> Self {
1486        self.details = Some(ErrorDetail::Text(detail.into()));
1487        self
1488    }
1489
1490    /// Attaches a binary detail payload to the error.
1491    pub fn with_detail_binary(mut self, detail: BinaryPayload) -> Self {
1492        self.details = Some(ErrorDetail::Binary(detail));
1493        self
1494    }
1495
1496    #[cfg(feature = "std")]
1497    /// Attaches a source error to the failure for debugging purposes.
1498    pub fn with_source<E>(mut self, source: E) -> Self
1499    where
1500        E: StdError + Send + Sync + 'static,
1501    {
1502        self.source = Some(Box::new(source));
1503        self
1504    }
1505
1506    /// Returns the structured details, when available.
1507    pub fn detail(&self) -> Option<&ErrorDetail> {
1508        self.details.as_ref()
1509    }
1510
1511    #[cfg(feature = "std")]
1512    /// Returns the attached source error when one has been provided.
1513    pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
1514        self.source.as_deref()
1515    }
1516}
1517
1518impl fmt::Display for NodeError {
1519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1520        write!(f, "{}: {}", self.code, self.message)
1521    }
1522}
1523
1524#[cfg(feature = "std")]
1525impl StdError for NodeError {
1526    fn source(&self) -> Option<&(dyn StdError + 'static)> {
1527        self.source
1528            .as_ref()
1529            .map(|err| err.as_ref() as &(dyn StdError + 'static))
1530    }
1531}
1532
1533/// Alias for results returned by node handlers.
1534pub type NodeResult<T> = Result<T, NodeError>;
1535
1536/// Generates a stable idempotency key for a node invocation.
1537///
1538/// The key uses tenant, flow, node, and correlation identifiers. Missing
1539/// correlation values fall back to the value stored on the context.
1540pub fn make_idempotency_key(
1541    ctx: &TenantCtx,
1542    flow_id: &str,
1543    node_id: Option<&str>,
1544    correlation: Option<&str>,
1545) -> String {
1546    let node_segment = node_id.unwrap_or_default();
1547    let correlation_segment = correlation
1548        .or(ctx.correlation_id.as_deref())
1549        .unwrap_or_default();
1550    let input = format!(
1551        "{}|{}|{}|{}",
1552        ctx.tenant_id.as_str(),
1553        flow_id,
1554        node_segment,
1555        correlation_segment
1556    );
1557    fnv1a_128_hex(input.as_bytes())
1558}
1559
1560const FNV_OFFSET_BASIS: u128 = 0x6c62272e07bb014262b821756295c58d;
1561const FNV_PRIME: u128 = 0x0000000001000000000000000000013b;
1562
1563fn fnv1a_128_hex(bytes: &[u8]) -> String {
1564    let mut hash = FNV_OFFSET_BASIS;
1565    for &byte in bytes {
1566        hash ^= byte as u128;
1567        hash = hash.wrapping_mul(FNV_PRIME);
1568    }
1569
1570    let mut output = String::with_capacity(32);
1571    for shift in (0..32).rev() {
1572        let nibble = ((hash >> (shift * 4)) & 0x0f) as u8;
1573        output.push(match nibble {
1574            0..=9 => (b'0' + nibble) as char,
1575            _ => (b'a' + (nibble - 10)) as char,
1576        });
1577    }
1578    output
1579}
1580
1581#[cfg(test)]
1582mod tests {
1583    use super::*;
1584    use core::convert::TryFrom;
1585    use time::OffsetDateTime;
1586
1587    fn sample_ctx() -> TenantCtx {
1588        let env = EnvId::try_from("prod").unwrap_or_else(|err| panic!("{err}"));
1589        let tenant = TenantId::try_from("tenant-123").unwrap_or_else(|err| panic!("{err}"));
1590        let team = TeamId::try_from("team-456").unwrap_or_else(|err| panic!("{err}"));
1591        let user = UserId::try_from("user-789").unwrap_or_else(|err| panic!("{err}"));
1592
1593        let mut ctx = TenantCtx::new(env, tenant)
1594            .with_team(Some(team))
1595            .with_user(Some(user))
1596            .with_attempt(2)
1597            .with_deadline(Some(InvocationDeadline::from_unix_millis(
1598                1_700_000_000_000,
1599            )));
1600        ctx.trace_id = Some("trace-abc".to_owned());
1601        ctx.correlation_id = Some("corr-xyz".to_owned());
1602        ctx.idempotency_key = Some("key-123".to_owned());
1603        ctx
1604    }
1605
1606    #[test]
1607    fn idempotent_key_stable() {
1608        let ctx = sample_ctx();
1609        let key_a = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1610        let key_b = make_idempotency_key(&ctx, "flow-1", Some("node-1"), Some("corr-override"));
1611        assert_eq!(key_a, key_b);
1612        assert_eq!(key_a.len(), 32);
1613    }
1614
1615    #[test]
1616    fn idempotent_key_uses_context_correlation() {
1617        let ctx = sample_ctx();
1618        let key = make_idempotency_key(&ctx, "flow-1", None, None);
1619        let expected = make_idempotency_key(&ctx, "flow-1", None, ctx.correlation_id.as_deref());
1620        assert_eq!(key, expected);
1621    }
1622
1623    #[test]
1624    #[cfg(feature = "time")]
1625    fn deadline_roundtrips_through_offset_datetime() {
1626        let dt = OffsetDateTime::from_unix_timestamp(1_700_000_000)
1627            .unwrap_or_else(|err| panic!("valid timestamp: {err}"));
1628        let deadline = InvocationDeadline::from_offset_date_time(dt);
1629        let roundtrip = deadline
1630            .to_offset_date_time()
1631            .unwrap_or_else(|err| panic!("round-trip conversion failed: {err}"));
1632        let millis = dt.unix_timestamp_nanos() / 1_000_000;
1633        assert_eq!(deadline.unix_millis(), millis);
1634        assert_eq!(roundtrip.unix_timestamp_nanos() / 1_000_000, millis);
1635    }
1636
1637    #[test]
1638    fn node_error_builder_sets_fields() {
1639        let err = NodeError::new("TEST", "example")
1640            .with_retry(Some(500))
1641            .with_detail_text("context");
1642
1643        assert!(err.retryable);
1644        assert_eq!(err.backoff_ms, Some(500));
1645        match err.detail() {
1646            Some(ErrorDetail::Text(detail)) => assert_eq!(detail, "context"),
1647            other => panic!("unexpected detail {other:?}"),
1648        }
1649    }
1650
1651    #[cfg(feature = "std")]
1652    #[test]
1653    fn node_error_source_roundtrips() {
1654        use std::io::Error;
1655
1656        let source = Error::other("boom");
1657        let err = NodeError::new("TEST", "example").with_source(source);
1658        assert!(err.source().is_some());
1659    }
1660}