wyrd-spec 0.1.1

Wyrd foundation types: Card envelope, SpecKinds, and pure data contracts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
//! Newtype identifiers used across Wyrd specs.

use std::fmt::{self, Display, Formatter, Result as FmtResult};
use std::str::FromStr;

use serde::{Deserialize, Deserializer, Serialize};
use uuid::Uuid;

macro_rules! id_type {
    ($name:ident, $doc:literal, $validator:ident) => {
        #[doc = $doc]
        #[derive(
            Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, schemars::JsonSchema,
        )]
        #[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
        pub struct $name(String);

        impl $name {
            /// Build a validated identifier.
            ///
            /// # Errors
            /// Returns an error when the identifier is not canonical for this type.
            pub fn new(value: impl Into<String>) -> Result<Self, IdError> {
                let value = value.into();
                $validator(&value)?;
                Ok(Self(value))
            }

            /// Borrow the identifier as a string.
            #[must_use]
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str(&self.0)
            }
        }

        impl FromStr for $name {
            type Err = IdError;

            fn from_str(value: &str) -> Result<Self, Self::Err> {
                Self::new(value)
            }
        }

        impl From<$name> for String {
            fn from(value: $name) -> Self {
                value.0
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
            where
                D: serde::Deserializer<'de>,
            {
                let value = String::deserialize(deserializer)?;
                Self::new(value).map_err(serde::de::Error::custom)
            }
        }
    };
}

id_type!(
    SpaceName,
    "Human-visible namespace for Cards and Runs.",
    validate_token
);
id_type!(CardName, "Human-visible Card name.", validate_token);
id_type!(CardUid, "Resolved immutable Card UID.", validate_uuid7);

impl CardUid {
    /// Build a [`CardUid`] from a UUIDv7 value.
    ///
    /// The UUID must be version 7; any other version returns [`IdError::InvalidUuid7`].
    ///
    /// # Errors
    /// Returns [`IdError`] when the UUID is not version 7.
    pub fn from_uuid(uuid: uuid::Uuid) -> Result<Self, IdError> {
        Self::new(uuid.to_string())
    }

    /// Parse the underlying string back to a [`uuid::Uuid`].
    ///
    /// # Panics
    /// Never: the constructor guarantees the string is a valid UUID.
    #[must_use]
    pub fn as_uuid(&self) -> uuid::Uuid {
        uuid::Uuid::parse_str(self.as_str()).expect("CardUid was validated at construction")
    }
}

id_type!(
    ProfileName,
    "Named execution or configuration profile.",
    validate_token
);
id_type!(
    ExperimentUid,
    "Resolved immutable Experiment UID.",
    validate_uuid7
);
id_type!(ApiToken, "Opaque API token identifier.", validate_opaque);
id_type!(
    IdempotencyKey,
    "Idempotency key for write operations.",
    validate_opaque
);
id_type!(ArtifactKey, "Artifact storage key.", validate_token);
id_type!(RoleName, "RBAC role identifier.", validate_token);
id_type!(
    ConnectionName,
    "Immutable tenant-scoped Operator connection name, unique per provider.",
    validate_token
);
id_type!(ColumnName, "DataCard column name.", validate_card_token);
id_type!(
    FeatureName,
    "Feature column name referenced by a drift Verifier signal.",
    validate_card_token
);
id_type!(
    MediaBindingId,
    "Name of a `${media:id}` binding slot in a resolved judge Prompt.",
    validate_card_token
);
id_type!(SplitName, "DataCard split label.", validate_card_token);
id_type!(QueryName, "DataCard SQL query key.", validate_card_token);
id_type!(
    TenantSlug,
    "Human-visible tenant slug resolved to a DataTenantId at the auth boundary.",
    validate_tenant_slug
);
id_type!(
    PodId,
    "Bifrost pod identifier for multi-pod seal coordination.",
    validate_token
);
id_type!(
    ProviderId,
    "Tenant-visible gateway provider identifier using the canonical Wyrd name grammar.",
    validate_token
);
id_type!(
    ModelId,
    "Provider-native gateway model identifier: 1..=255 UTF-8 bytes without control characters.",
    validate_model_id
);
id_type!(
    ProviderCredentialName,
    "Tenant-unique gateway provider credential name.",
    validate_token
);
id_type!(
    ProviderDeploymentName,
    "Tenant-unique gateway provider deployment name.",
    validate_token
);
id_type!(
    CredentialBindingName,
    "Operator-configured gateway credential binding name.",
    validate_token
);
id_type!(
    SecretBackendName,
    "Operator-configured external secret backend identity.",
    validate_token
);

/// Immutable tenant isolation key used by tenant-scoped Wyrd and Vala rows.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, schemars::JsonSchema,
)]
#[serde(transparent)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct DataTenantId(uuid::Uuid);

impl DataTenantId {
    /// Fixed `UUIDv7` of the platform system tenant (`wyrd-system`).
    ///
    /// It owns system-attributed data such as audit decisions that cannot be
    /// attributed to a caller's tenant. It is an ordinary valid tenant id, so
    /// every decoder accepts it without a special case.
    pub const SYSTEM_OWNER: DataTenantId = DataTenantId(uuid::Uuid::from_u128(
        0x0000_0000_0000_7000_8000_0000_0000_0000,
    ));

    /// Generate a UUIDv7-backed tenant isolation key.
    #[must_use]
    pub fn new_v7() -> Self {
        Self(uuid::Uuid::now_v7())
    }

    /// Build a tenant isolation key from a UUIDv7 value.
    ///
    /// # Errors
    /// Returns an error when the UUID is not version 7.
    pub fn new(value: uuid::Uuid) -> Result<Self, IdError> {
        if value.get_version_num() == 7 {
            Ok(Self(value))
        } else {
            Err(IdError::InvalidUuid7)
        }
    }

    /// Borrow the underlying UUID.
    #[must_use]
    pub fn as_uuid(&self) -> uuid::Uuid {
        self.0
    }
}

impl fmt::Display for DataTenantId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl FromStr for DataTenantId {
    type Err = IdError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let uuid = uuid::Uuid::parse_str(value).map_err(|_| IdError::InvalidUuid7)?;
        Self::new(uuid)
    }
}

impl TryFrom<uuid::Uuid> for DataTenantId {
    type Error = IdError;

    fn try_from(value: uuid::Uuid) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl From<DataTenantId> for uuid::Uuid {
    fn from(value: DataTenantId) -> Self {
        value.0
    }
}

impl<'de> Deserialize<'de> for DataTenantId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = uuid::Uuid::deserialize(deserializer)?;
        Self::new(value).map_err(serde::de::Error::custom)
    }
}

/// Define a server-minted `UUIDv7` identity newtype.
///
/// Every generated type serializes as the canonical hyphenated UUID, mints
/// fresh values with [`Uuid::now_v7`], and refuses any non-v7 UUID on
/// construction, parsing, and deserialization, so a stored or submitted
/// identity of another version never becomes a typed value.
macro_rules! uuid7_id_type {
    ($(#[$meta:meta])* $name:ident) => {
        $(#[$meta])*
        #[derive(
            Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize,
            schemars::JsonSchema,
        )]
        #[serde(transparent)]
        #[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
        pub struct $name(Uuid);

        impl $name {
            /// Mint a fresh identity.
            #[must_use]
            pub fn new_v7() -> Self {
                Self(uuid::Uuid::now_v7())
            }

            /// Build an identity from a stored or decoded UUID.
            ///
            /// # Errors
            /// Returns [`IdError::InvalidUuid7`] when the UUID is not version 7.
            pub fn new(value: Uuid) -> Result<Self, IdError> {
                if value.get_version_num() == 7 {
                    Ok(Self(value))
                } else {
                    Err(IdError::InvalidUuid7)
                }
            }

            /// Return the underlying UUID.
            #[must_use]
            pub fn as_uuid(&self) -> Uuid {
                self.0
            }
        }

        impl Display for $name {
            /// Render the canonical hyphenated UUID form used on the wire.
            fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
                Display::fmt(&self.0, f)
            }
        }

        impl FromStr for $name {
            /// The identifier refusal a malformed or non-v7 identity produces.
            type Err = IdError;

            /// Parse a path or query value, refusing a malformed or non-v7 UUID.
            ///
            /// # Errors
            /// Returns [`IdError::InvalidUuid7`] when `value` is not a UUID or
            /// is a UUID of any version other than 7.
            fn from_str(value: &str) -> Result<Self, Self::Err> {
                let uuid = uuid::Uuid::parse_str(value).map_err(|_| IdError::InvalidUuid7)?;
                Self::new(uuid)
            }
        }

        impl<'de> Deserialize<'de> for $name {
            /// Decode a UUID and refuse any version other than 7.
            ///
            /// # Errors
            /// Returns the deserializer's error when the value is not a UUID,
            /// or a custom error carrying [`IdError::InvalidUuid7`] for a
            /// non-v7 UUID.
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
            where
                D: Deserializer<'de>,
            {
                let value = uuid::Uuid::deserialize(deserializer)?;
                Self::new(value).map_err(serde::de::Error::custom)
            }
        }
    };
}

uuid7_id_type!(
    /// Durable identity of one projected verification binding.
    ///
    /// A binding is an inline `verified_by` entry on a Service, one of its
    /// component occurrences, or a standalone Agent; it is never authored with
    /// its own name or identifier. Registration mints this `UUIDv7` on the
    /// first projection of the binding's natural key — tenant, exact owner
    /// Card UID, subject occurrence, and exact Verifier UID — and every later
    /// projection of that same key keeps it, so status, runs, and results
    /// address the binding stably.
    BindingId
);

uuid7_id_type!(
    /// Durable identity of one Verifier run in `wyrd.verifier_runs`.
    ///
    /// The server mints it when scheduled, manual, or observation work is
    /// enqueued. It is the managed `run_id` of every result and detail row the
    /// run publishes and addresses the run's status and Operator dispatches.
    /// Retries and lease reclaims keep it.
    VerificationRunId
);

uuid7_id_type!(
    /// Durable identity of one canonical Verification Result.
    ///
    /// The runner mints it for a completed run's result batches: the summary
    /// row in `vala.verification.results` and every detail row join on
    /// (`data_tenant_id`, `result_id`), and the settled run points at it.
    VerificationResultId
);

uuid7_id_type!(
    /// Transient identity of one direct Verifier execution.
    ///
    /// `POST /v1/verification/execute` mints it per request. It appears only
    /// in that response, the request's audit decision, its trace, and as the
    /// `result_id` of the execution's recorded result rows.
    VerificationExecutionId
);

uuid7_id_type!(
    /// Durable identity of one Operator dispatch in `wyrd.operator_dispatches`.
    ///
    /// Settlement of a failed binding-created run mints one per distinct
    /// configured Operator. Every delivery retry keeps it, so it is also the
    /// idempotency key an external destination may honor.
    OperatorDispatchId
);

uuid7_id_type!(
    /// Durable identity of one tenant Operator connection.
    ///
    /// The server mints it on create; secret rotation, metadata updates,
    /// disable, and re-enable keep it, so Cards name the connection by its
    /// immutable name while management surfaces address it by this ID.
    OperatorConnectionId
);

/// Generate a UUIDv7 string for Wyrd-owned identifiers.
#[must_use]
pub fn uuid7() -> String {
    uuid::Uuid::new_v7(uuid::Timestamp::now(uuid::NoContext)).to_string()
}

fn validate_token(value: &str) -> Result<(), IdError> {
    validate_token_with_min(value, 3)
}

fn validate_card_token(value: &str) -> Result<(), IdError> {
    validate_token_with_min(value, 1)
}

fn validate_token_with_min(value: &str, min_len: usize) -> Result<(), IdError> {
    if value.len() < min_len || value.len() > 64 {
        return Err(IdError::InvalidToken);
    }
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err(IdError::InvalidToken);
    };
    if !first.is_ascii_lowercase() {
        return Err(IdError::InvalidToken);
    }
    if chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-') {
        Ok(())
    } else {
        Err(IdError::InvalidToken)
    }
}

fn validate_tenant_slug(value: &str) -> Result<(), IdError> {
    if value.is_empty() || value.len() > 63 {
        return Err(IdError::InvalidTenantSlug);
    }
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return Err(IdError::InvalidTenantSlug);
    };
    if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
        return Err(IdError::InvalidTenantSlug);
    }
    if chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-') {
        Ok(())
    } else {
        Err(IdError::InvalidTenantSlug)
    }
}

fn validate_uuid7(value: &str) -> Result<(), IdError> {
    let uuid = uuid::Uuid::parse_str(value).map_err(|_| IdError::InvalidUuid7)?;
    if uuid.get_version_num() == 7 {
        Ok(())
    } else {
        Err(IdError::InvalidUuid7)
    }
}

/// Accepts a provider-native model identifier.
///
/// Provider catalogs are open-ended, so the grammar only bounds the value to
/// 1..=255 UTF-8 bytes and rejects control characters; slashes are allowed.
///
/// # Errors
///
/// Returns [`IdError::InvalidModelId`] for an empty, over-long, or
/// control-character-bearing value.
fn validate_model_id(value: &str) -> Result<(), IdError> {
    if value.is_empty() || value.len() > 255 || value.chars().any(char::is_control) {
        return Err(IdError::InvalidModelId);
    }
    Ok(())
}

fn validate_opaque(value: &str) -> Result<(), IdError> {
    if value.len() < 8 || value.len() > 256 || value.chars().any(char::is_whitespace) {
        return Err(IdError::InvalidOpaque);
    }
    Ok(())
}

/// Identifier validation failures.
#[derive(Debug, thiserror::Error)]
pub enum IdError {
    /// Human-readable token did not match `[a-z][a-z0-9_-]{2,63}`.
    #[error("identifier must match [a-z][a-z0-9_-]{{2,63}}")]
    InvalidToken,
    /// UID was not a UUIDv7.
    #[error("identifier must be a UUIDv7")]
    InvalidUuid7,
    /// Tenant slug did not match `[a-z0-9][a-z0-9_-]{0,62}`.
    #[error("tenant slug must match [a-z0-9][a-z0-9_-]{{0,62}}")]
    InvalidTenantSlug,
    /// Opaque identifier was empty, too long, too short, or contained whitespace.
    #[error("opaque identifier is invalid")]
    InvalidOpaque,
    /// Model identifier was empty, longer than 255 bytes, or held a control character.
    #[error("model identifier must be 1..=255 UTF-8 bytes without control characters")]
    InvalidModelId,
}