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