- 1
//! Authority: what a human has **delegated**, as distinct from what the - 2
//! request happens to need. - 3
//! - 4
//! # Why this is separate from the reading - 5
//! - 6
//! [`crate::Reading`] is inferred from the request. Authority is granted by a - 7
//! person. Keeping them apart is the whole point: a resolution bug can change - 8
//! what vak *thinks the work is*, and must never change *what it is allowed to - 9
//! do about it*. - 10
//! - 11
//! # How this composes with the permission engine - 12
//! - 13
//! It does not replace it, and it cannot outvote it. There are two separate - 14
//! questions and vak had been treating them as one: - 15
//! - 16
//! 1. **Is a gate raised?** Decided by the permission engine, exactly as - 17
//! today, and then *possibly tightened* by [`Authority::approval_ceiling`]. - 18
//! Autonomy can suppress a gate the engine already made optional; it can - 19
//! never suppress a `Deny`, and it can never turn a `Deny` into an `Ask`. - 20
//! 2. **Can a raised gate be answered?** Decided by [`Attendance`] and the - 21
//! hosting surface's approver. This is where an unattended surface used to - 22
//! fail closed unconditionally — correct for a one-shot turn, wrong for - 23
//! month-long work, which should *wait* rather than *fail*. See - 24
//! [`GateFallback`]. - 25
//! - 26
//! Both directions only ever narrow. `ApprovalCeiling` is a cap on - 27
//! permissiveness, and an [`Envelope`] carries a `permission_ceiling` that can - 28
//! lower the effective mode but never raise it — pre-authorization *within* - 29
//! existing authority, never a grant of new authority. - 30
- 31
use serde::{Deserialize, Serialize}; - 32
- 33
use crate::axes::{Attendance, Horizon, Stakes}; - 34
- 35
/// How much authority a human has delegated for this work. - 36
/// - 37
/// Ordered least to most delegated. Note that this is *not* capped by - 38
/// [`Attendance`]: how much you have delegated and whether you are watching - 39
/// are independent facts. Their interaction is handled where it belongs — in - 40
/// [`Authority::gate_fallback`], which decides what happens to a gate nobody - 41
/// is there to answer. - 42
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)] - 43
#[serde(rename_all = "kebab-case")] - 44
pub enum Autonomy { - 45
/// Propose only. Every effect is approved before it happens. - 46
Manual, - 47
/// Act on reversible things; ask before anything costly or irreversible. - 48
#[default] - 49
Assisted, - 50
/// Act freely inside a declared [`Envelope`]; escalate outside it. - 51
Delegated, - 52
/// Act freely within the permission mode and report afterwards. - 53
Autonomous, - 54
} - 55
- 56
impl Autonomy { - 57
pub const ALL: [Autonomy; 4] = [ - 58
Autonomy::Manual, - 59
Autonomy::Assisted, - 60
Autonomy::Delegated, - 61
Autonomy::Autonomous, - 62
]; - 63
- 64
/// Explicit total order; spelled out rather than derived so reordering - 65
/// the variants cannot silently widen a grant. - 66
pub fn rank(self) -> u8 { - 67
match self { - 68
Autonomy::Manual => 0, - 69
Autonomy::Assisted => 1, - 70
Autonomy::Delegated => 2, - 71
Autonomy::Autonomous => 3, - 72
} - 73
} - 74
- 75
pub fn as_str(self) -> &'static str { - 76
match self { - 77
Autonomy::Manual => "manual", - 78
Autonomy::Assisted => "assisted", - 79
Autonomy::Delegated => "delegated", - 80
Autonomy::Autonomous => "autonomous", - 81
} - 82
} - 83
- 84
pub fn parse(value: &str) -> Option<Autonomy> { - 85
Autonomy::ALL - 86
.into_iter() - 87
.find(|a| a.as_str() == value.trim().to_ascii_lowercase()) - 88
} - 89
- 90
/// The least delegated of the two. Used wherever two grants meet — a - 91
/// per-chat grant under a per-bot grant, or a worker under its parent — - 92
/// so composition can only ever reduce. - 93
pub fn capped_by(self, ceiling: Autonomy) -> Autonomy { - 94
if self.rank() > ceiling.rank() { - 95
ceiling - 96
} else { - 97
self - 98
} - 99
} - 100
} - 101
- 102
/// A cap on how permissive approval handling may be. - 103
/// - 104
/// Mirrors the ranking of `vak_config::ApprovalMode` (`ask` < `approve-safe` < - 105
/// `auto-approve`). It is duplicated rather than imported so the intent kernel - 106
/// stays a pure decision layer with no configuration dependency and can be - 107
/// tested standalone; `vak-core` maps between the two in exactly one place. - 108
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)] - 109
#[serde(rename_all = "kebab-case")] - 110
pub enum ApprovalCeiling { - 111
/// Every gate reaches a human. - 112
Ask, - 113
/// Gates the engine considers safe may resolve themselves. - 114
ApproveSafe, - 115
/// Gates may resolve themselves. - 116
#[default] - 117
AutoApprove, - 118
} - 119
- 120
impl ApprovalCeiling { - 121
pub const ALL: [ApprovalCeiling; 3] = [ - 122
ApprovalCeiling::Ask, - 123
ApprovalCeiling::ApproveSafe, - 124
ApprovalCeiling::AutoApprove, - 125
]; - 126
- 127
pub fn rank(self) -> u8 { - 128
match self { - 129
ApprovalCeiling::Ask => 0, - 130
ApprovalCeiling::ApproveSafe => 1, - 131
ApprovalCeiling::AutoApprove => 2, - 132
} - 133
} - 134
- 135
pub fn as_str(self) -> &'static str { - 136
match self { - 137
ApprovalCeiling::Ask => "ask", - 138
ApprovalCeiling::ApproveSafe => "approve-safe", - 139
ApprovalCeiling::AutoApprove => "auto-approve", - 140
} - 141
} - 142
- 143
pub fn parse(value: &str) -> Option<ApprovalCeiling> { - 144
match value.trim().to_ascii_lowercase().as_str() { - 145
"ask" | "ask-approval" => Some(ApprovalCeiling::Ask), - 146
"approve-safe" | "approve-for-me" => Some(ApprovalCeiling::ApproveSafe), - 147
"auto-approve" => Some(ApprovalCeiling::AutoApprove), - 148
_ => None, - 149
} - 150
} - 151
- 152
/// The stricter of the two. Every composition point uses this, which is - 153
/// what makes "intent may tighten approval, never loosen it" hold by - 154
/// construction rather than by review. - 155
pub fn meet(self, other: ApprovalCeiling) -> ApprovalCeiling { - 156
if other.rank() < self.rank() { - 157
other - 158
} else { - 159
self - 160
} - 161
} - 162
} - 163
- 164
/// A cap on the permission mode. - 165
/// - 166
/// Mirrors `vak_config::PermissionMode`'s ranking for the same reason - 167
/// [`ApprovalCeiling`] mirrors `ApprovalMode`. - 168
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)] - 169
#[serde(rename_all = "kebab-case")] - 170
pub enum PermissionCeiling { - 171
ReadOnly, - 172
WorkspaceWrite, - 173
#[default] - 174
FullAccess, - 175
} - 176
- 177
impl PermissionCeiling { - 178
pub const ALL: [PermissionCeiling; 3] = [ - 179
PermissionCeiling::ReadOnly, - 180
PermissionCeiling::WorkspaceWrite, - 181
PermissionCeiling::FullAccess, - 182
]; - 183
- 184
pub fn rank(self) -> u8 { - 185
match self { - 186
PermissionCeiling::ReadOnly => 0, - 187
PermissionCeiling::WorkspaceWrite => 1, - 188
PermissionCeiling::FullAccess => 2, - 189
} - 190
} - 191
- 192
pub fn as_str(self) -> &'static str { - 193
match self { - 194
PermissionCeiling::ReadOnly => "read-only", - 195
PermissionCeiling::WorkspaceWrite => "workspace-write", - 196
PermissionCeiling::FullAccess => "full-access", - 197
} - 198
} - 199
- 200
pub fn parse(value: &str) -> Option<PermissionCeiling> { - 201
match value.trim().to_ascii_lowercase().as_str() { - 202
"read-only" | "readonly" => Some(PermissionCeiling::ReadOnly), - 203
"workspace-write" => Some(PermissionCeiling::WorkspaceWrite), - 204
"full-access" | "fullaccess" => Some(PermissionCeiling::FullAccess), - 205
_ => None, - 206
} - 207
} - 208
- 209
pub fn meet(self, other: PermissionCeiling) -> PermissionCeiling { - 210
if other.rank() < self.rank() { - 211
other - 212
} else { - 213
self - 214
} - 215
} - 216
} - 217
- 218
/// What happens to a gate that was raised and cannot be answered here. - 219
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] - 220
#[serde(rename_all = "kebab-case")] - 221
pub enum GateFallback { - 222
/// Fail closed. The historical behaviour, and still correct for a - 223
/// one-shot turn with nowhere to park the question. - 224
Deny, - 225
/// Suspend the commitment on a `Human` wake condition and put the - 226
/// question in the inbox. Nothing happens without the answer, but the - 227
/// work survives to be resumed — which is what long-horizon work needs - 228
/// and what a hard denial destroys. - 229
Defer, - 230
} - 231
- 232
/// What to do when a deferred question goes unanswered past its deadline. - 233
/// - 234
/// A deferred question with no timeout policy is how an agent quietly - 235
/// accumulates a graveyard of half-finished work, so this is not optional. - 236
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - 237
#[serde(tag = "kind", rename_all = "kebab-case")] - 238
pub enum Escalation { - 239
/// Keep waiting. Appropriate when there is no safe default, and the - 240
/// default for exactly that reason. - 241
#[default] - 242
WaitIndefinitely, - 243
/// Take the conservative branch and record that it was taken without an - 244
/// answer. Never available above `Stakes::Costly`. - 245
AssumeConservative { after_hours: u32 }, - 246
/// Give up and close the commitment as `Abandoned`. - 247
AbandonAfter { after_hours: u32 }, - 248
/// Ask somebody else. - 249
Reassign { to: String, after_hours: u32 }, - 250
} - 251
- 252
impl Escalation { - 253
/// Whether this policy may be applied to work at `stakes`. - 254
/// - 255
/// Assuming a default for an irreversible action because nobody replied - 256
/// is exactly the class of autonomy this system exists to prevent, so the - 257
/// restriction is enforced rather than documented. - 258
pub fn permitted_for(&self, stakes: Stakes) -> bool { - 259
match self { - 260
Escalation::AssumeConservative { .. } => stakes.rank() <= Stakes::Costly.rank(), - 261
_ => true, - 262
} - 263
} - 264
} - 265
- 266
/// A pre-authorization attached to a commitment. - 267
/// - 268
/// An envelope answers "what may you do without asking me again, and until - 269
/// when". It is the mechanism that makes unattended long-horizon work possible - 270
/// without either nagging or recklessness, and it is the shape EU AI Act - 271
/// Article 14 human oversight actually wants: a boundary agreed up front with - 272
/// a defined escalation, rather than per-action interrupts that fatigue an - 273
/// overseer into rubber-stamping. - 274
/// - 275
/// It can only narrow. `permission_ceiling` lowers the effective mode and - 276
/// never raises it; `spend_limit_usd` lowers the effective budget; the scopes - 277
/// restrict rather than grant. - 278
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 279
pub struct Envelope { - 280
pub envelope_id: String, - 281
/// Who granted it. Recorded because "under whose authority" is the - 282
/// question an audit asks first. - 283
pub granted_by: String, - 284
pub granted_at: chrono::DateTime<chrono::Utc>, - 285
#[serde(default, skip_serializing_if = "Option::is_none")] - 286
pub expires_at: Option<chrono::DateTime<chrono::Utc>>, - 287
/// Lifetime spend for the whole commitment, not per run. - 288
#[serde(default, skip_serializing_if = "Option::is_none")] - 289
pub spend_limit_usd: Option<f64>, - 290
/// Workspace-relative path globs the grant covers. Empty means "no path - 291
/// restriction beyond the permission mode's own" — it does not mean - 292
/// "every path", because the mode is still in force. - 293
#[serde(default)] - 294
pub path_scope: Vec<String>, - 295
/// Tool names the grant covers. Empty means no additional restriction. - 296
#[serde(default)] - 297
pub tool_scope: Vec<String>, - 298
/// Never above the mode already in force. - 299
#[serde(default)] - 300
pub permission_ceiling: PermissionCeiling, - 301
#[serde(default)] - 302
pub escalation: Escalation, - 303
#[serde(default, skip_serializing_if = "Option::is_none")] - 304
pub revoked_at: Option<chrono::DateTime<chrono::Utc>>, - 305
} - 306
- 307
impl Envelope { - 308
/// Whether the envelope is in force at `now`. - 309
/// - 310
/// Revocation and expiry are checked here rather than at the call sites so - 311
/// there is exactly one place that can get it wrong. - 312
pub fn is_live(&self, now: chrono::DateTime<chrono::Utc>) -> bool { - 313
if self.revoked_at.is_some() { - 314
return false; - 315
} - 316
match self.expires_at { - 317
Some(expiry) => now < expiry, - 318
None => true, - 319
} - 320
} - 321
- 322
/// Whether an action on `tool` touching `paths` falls inside the grant. - 323
/// - 324
/// An empty scope is "no additional restriction", not "everything": the - 325
/// permission mode and rule engine are still the authority. A path that - 326
/// cannot be matched against the scope counts as outside it, because an - 327
/// unprovable claim of coverage is not coverage — the same reasoning the - 328
/// permission engine uses when redirection makes a compound command's - 329
/// coverage unprovable. - 330
pub fn covers(&self, tool: &str, paths: &[String]) -> bool { - 331
if !self.tool_scope.is_empty() && !self.tool_scope.iter().any(|t| t == tool) { - 332
return false; - 333
} - 334
if self.path_scope.is_empty() { - 335
return true; - 336
} - 337
!paths.is_empty() - 338
&& paths.iter().all(|path| { - 339
workspace_relative(path) - 340
.is_some_and(|path| self.path_scope.iter().any(|glob| glob_covers(glob, &path))) - 341
}) - 342
} - 343
} - 344
- 345
/// A path the scope can be matched against: workspace-relative, with no - 346
/// parent-directory step. `src/../.env` would otherwise match `src/**` - 347
/// byte by byte, so anything that climbs, is absolute, or uses a Windows - 348
/// separator is not coverable at all. - 349
fn workspace_relative(path: &str) -> Option<String> { - 350
let path = path.trim(); - 351
if path.is_empty() || path.starts_with('/') || path.contains('\\') || path.contains(':') { - 352
return None; - 353
} - 354
let mut parts = Vec::new(); - 355
for part in path.split('/') { - 356
match part { - 357
"" | "." => {} - 358
".." => return None, - 359
part => parts.push(part), - 360
} - 361
} - 362
(!parts.is_empty()).then(|| parts.join("/")) - 363
} - 364
- 365
/// Minimal `*`/`**` glob matching for envelope path scopes. - 366
/// - 367
/// Deliberately conservative: anything it cannot prove is covered reads as not - 368
/// covered, so a scope mistake denies rather than grants. - 369
fn glob_covers(pattern: &str, path: &str) -> bool { - 370
fn matches(pattern: &[u8], path: &[u8]) -> bool { - 371
match pattern.first() { - 372
None => path.is_empty(), - 373
Some(b'*') => { - 374
// `**` spans separators; a single `*` stops at one. - 375
let doubled = pattern.get(1) == Some(&b'*'); - 376
let rest = if doubled { - 377
&pattern[2..] - 378
} else { - 379
&pattern[1..] - 380
}; - 381
let rest = if doubled && rest.first() == Some(&b'/') { - 382
&rest[1..] - 383
} else { - 384
rest - 385
}; - 386
if matches(rest, path) { - 387
return true; - 388
} - 389
for (index, byte) in path.iter().enumerate() { - 390
if !doubled && *byte == b'/' { - 391
break; - 392
} - 393
if matches(rest, &path[index + 1..]) { - 394
return true; - 395
} - 396
} - 397
false - 398
} - 399
Some(expected) => match path.first() { - 400
Some(actual) if actual == expected => matches(&pattern[1..], &path[1..]), - 401
_ => false, - 402
}, - 403
} - 404
} - 405
matches(pattern.as_bytes(), path.as_bytes()) - 406
} - 407
- 408
/// The composed authority for a unit of work. - 409
/// - 410
/// A grant on a commitment is not part of it: which commitment a strand - 411
/// works on is only known after the request is read, so an envelope narrows - 412
/// the strands that serve its commitment (`apply_envelopes`) and covers - 413
/// individual actions at the approval gate (`Envelope::covers`). - 414
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 415
pub struct Authority { - 416
pub autonomy: Autonomy, - 417
/// Observed, not granted. - 418
pub attendance: Attendance, - 419
} - 420
- 421
impl Default for Authority { - 422
fn default() -> Self { - 423
Authority { - 424
autonomy: Autonomy::Assisted, - 425
attendance: Attendance::Interactive, - 426
} - 427
} - 428
} - 429
- 430
impl Authority { - 431
/// The cap this authority places on approval permissiveness for an action - 432
/// at `stakes`. - 433
/// - 434
/// Delegation buys nothing here: a turn's ceiling is fixed before anyone - 435
/// knows which actions it will take, so `delegated` asks, and the gate - 436
/// lets through only the actions a live envelope covers — outside the - 437
/// boundary, delegation buys nothing, which is the point of declaring it. - 438
/// - 439
/// The result is a *ceiling*: `vak-core` takes the stricter of this and - 440
/// the configured `ApprovalMode`. Nothing here can loosen configuration. - 441
pub fn approval_ceiling(&self, stakes: Stakes) -> ApprovalCeiling { - 442
let by_stakes = match stakes { - 443
Stakes::Inert | Stakes::Reversible => ApprovalCeiling::AutoApprove, - 444
Stakes::Costly => ApprovalCeiling::ApproveSafe, - 445
// Irreversible always reaches a human, whatever was delegated. - 446
// A grant to act without asking is not a grant to act without - 447
// anyone ever knowing. - 448
Stakes::Irreversible => ApprovalCeiling::Ask, - 449
}; - 450
let by_autonomy = match self.autonomy { - 451
Autonomy::Manual => ApprovalCeiling::Ask, - 452
// "Act on reversible things; ask before anything costly or - 453
// irreversible" — as the variant's own docstring says. The - 454
// earlier mapping capped *every* stakes level at `approve-safe`, - 455
// which made `assisted` indistinguishable from `autonomous` on - 456
// costly work and silently downgraded an operator's - 457
// `auto-approve` on a greeting. - 458
Autonomy::Assisted => { - 459
if stakes.rank() >= Stakes::Costly.rank() { - 460
ApprovalCeiling::Ask - 461
} else { - 462
ApprovalCeiling::AutoApprove - 463
} - 464
} - 465
Autonomy::Delegated => ApprovalCeiling::Ask, - 466
Autonomy::Autonomous => ApprovalCeiling::AutoApprove, - 467
}; - 468
by_stakes.meet(by_autonomy) - 469
} - 470
- 471
/// What to do with a gate that was raised and cannot be answered here. - 472
/// - 473
/// Deferring needs somewhere to park the question, so it is only offered - 474
/// when the work is durable enough to own a commitment. A one-shot - 475
/// unattended turn still fails closed, exactly as before. - 476
pub fn gate_fallback(&self, horizon: Horizon) -> GateFallback { - 477
if self.attendance.can_answer_now() { - 478
// Somebody is here; the gate resolves the ordinary way and this - 479
// fallback never applies. - 480
return GateFallback::Deny; - 481
} - 482
if horizon.opens_commitment() { - 483
GateFallback::Defer - 484
} else { - 485
GateFallback::Deny - 486
} - 487
} - 488
} - 489
- 490
#[cfg(test)] - 491
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 492
mod tests { - 493
use super::*; - 494
- 495
fn envelope() -> Envelope { - 496
Envelope { - 497
envelope_id: "env-1".into(), - 498
granted_by: "nisheeth".into(), - 499
granted_at: chrono::Utc::now(), - 500
expires_at: None, - 501
spend_limit_usd: Some(5.0), - 502
path_scope: vec!["src/**".into()], - 503
tool_scope: vec!["edit".into(), "write".into()], - 504
permission_ceiling: PermissionCeiling::WorkspaceWrite, - 505
escalation: Escalation::WaitIndefinitely, - 506
revoked_at: None, - 507
} - 508
} - 509
- 510
/// The single most important property in this module: no combination of - 511
/// delegation can let an irreversible action past without a human. - 512
#[test] - 513
fn irreversible_always_reaches_a_human_whatever_was_delegated() { - 514
for autonomy in Autonomy::ALL { - 515
for attendance in Attendance::ALL { - 516
let authority = Authority { - 517
autonomy, - 518
attendance, - 519
}; - 520
assert_eq!( - 521
authority.approval_ceiling(Stakes::Irreversible), - 522
ApprovalCeiling::Ask, - 523
"autonomy={autonomy:?}" - 524
); - 525
} - 526
} - 527
} - 528
- 529
/// The autonomy table as documented: assisted acts on reversible work - 530
/// without a gate and asks before anything costly. - 531
#[test] - 532
fn assisted_auto_approves_reversible_and_asks_for_costly() { - 533
let assisted = Authority::default(); - 534
assert_eq!( - 535
assisted.approval_ceiling(Stakes::Inert), - 536
ApprovalCeiling::AutoApprove - 537
); - 538
assert_eq!( - 539
assisted.approval_ceiling(Stakes::Reversible), - 540
ApprovalCeiling::AutoApprove - 541
); - 542
assert_eq!( - 543
assisted.approval_ceiling(Stakes::Costly), - 544
ApprovalCeiling::Ask - 545
); - 546
let autonomous = Authority { - 547
autonomy: Autonomy::Autonomous, - 548
..Authority::default() - 549
}; - 550
assert_eq!( - 551
autonomous.approval_ceiling(Stakes::Costly), - 552
ApprovalCeiling::ApproveSafe - 553
); - 554
} - 555
- 556
/// A turn under delegation asks: only the actions a live envelope covers - 557
/// get through, and that is decided per action at the gate. - 558
#[test] - 559
fn delegation_alone_buys_nothing_at_the_turn_level() { - 560
let authority = Authority { - 561
autonomy: Autonomy::Delegated, - 562
attendance: Attendance::Supervised, - 563
}; - 564
for stakes in [Stakes::Inert, Stakes::Reversible, Stakes::Costly] { - 565
assert_eq!(authority.approval_ceiling(stakes), ApprovalCeiling::Ask); - 566
} - 567
} - 568
- 569
#[test] - 570
fn approval_ceiling_only_ever_tightens_when_composed() { - 571
for a in ApprovalCeiling::ALL { - 572
for b in ApprovalCeiling::ALL { - 573
let met = a.meet(b); - 574
assert!(met.rank() <= a.rank() && met.rank() <= b.rank()); - 575
} - 576
} - 577
} - 578
- 579
#[test] - 580
fn revoked_and_expired_envelopes_are_not_live() { - 581
let now = chrono::Utc::now(); - 582
let mut revoked = envelope(); - 583
revoked.revoked_at = Some(now); - 584
assert!(!revoked.is_live(now)); - 585
- 586
let mut expired = envelope(); - 587
expired.expires_at = Some(now - chrono::Duration::hours(1)); - 588
assert!(!expired.is_live(now)); - 589
assert!(envelope().is_live(now)); - 590
} - 591
- 592
#[test] - 593
fn envelope_coverage_is_conservative() { - 594
let envelope = envelope(); - 595
assert!(envelope.covers("edit", &["src/main.rs".into()])); - 596
assert!(envelope.covers("edit", &["src/deep/nested/file.rs".into()])); - 597
// Wrong tool. - 598
assert!(!envelope.covers("bash", &["src/main.rs".into()])); - 599
// Outside the path scope. - 600
assert!(!envelope.covers("edit", &["docs/readme.md".into()])); - 601
// One covered and one not is not coverage. - 602
assert!(!envelope.covers("edit", &["src/main.rs".into(), "docs/x.md".into()])); - 603
// A scoped envelope with nothing to check cannot prove coverage. - 604
assert!(!envelope.covers("edit", &[])); - 605
// A path that climbs out of the scope, or is absolute, is not - 606
// covered by a pattern it happens to start with. - 607
assert!(!envelope.covers("edit", &["src/../.env".into()])); - 608
assert!(!envelope.covers("edit", &["/etc/src/main.rs".into()])); - 609
assert!(envelope.covers("edit", &["./src/main.rs".into()])); - 610
} - 611
- 612
#[test] - 613
fn unattended_durable_work_defers_instead_of_failing_closed() { - 614
let authority = Authority { - 615
autonomy: Autonomy::Delegated, - 616
attendance: Attendance::Unattended, - 617
}; - 618
assert_eq!( - 619
authority.gate_fallback(Horizon::Durable), - 620
GateFallback::Defer - 621
); - 622
// A one-shot turn has nowhere to park the question, so it still fails - 623
// closed exactly as it did before. - 624
assert_eq!( - 625
authority.gate_fallback(Horizon::Immediate), - 626
GateFallback::Deny - 627
); - 628
} - 629
- 630
#[test] - 631
fn conservative_assumption_is_refused_for_irreversible_work() { - 632
let policy = Escalation::AssumeConservative { after_hours: 24 }; - 633
assert!(policy.permitted_for(Stakes::Reversible)); - 634
assert!(policy.permitted_for(Stakes::Costly)); - 635
assert!(!policy.permitted_for(Stakes::Irreversible)); - 636
assert!(Escalation::WaitIndefinitely.permitted_for(Stakes::Irreversible)); - 637
} - 638
- 639
#[test] - 640
fn autonomy_composition_only_reduces() { - 641
for a in Autonomy::ALL { - 642
for b in Autonomy::ALL { - 643
let capped = a.capped_by(b); - 644
assert!(capped.rank() <= a.rank() && capped.rank() <= b.rank()); - 645
} - 646
} - 647
} - 648
} - 649
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.