greentic_types/
lib.rs

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