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