greentic_types/
lib.rs

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