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