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