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