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