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