- 1
//! Projection invariants: nothing the intent kernel derives may widen what a - 2
//! turn is allowed to do (`AGENTS.md` invariant 32), and a grant may only - 3
//! narrow (invariant 3 in docs/design/47). - 4
- 5
#![allow(clippy::unwrap_used, clippy::expect_used)] - 6
- 7
use vak_core::intent; - 8
use vak_intent::{ - 9
ApprovalCeiling, Authority, Autonomy, DomainSet, Envelope, Escalation, PermissionCeiling, - 10
Stakes, - 11
}; - 12
- 13
fn envelope(ceiling: PermissionCeiling) -> Envelope { - 14
Envelope { - 15
envelope_id: "env-1".into(), - 16
granted_by: "nisheeth".into(), - 17
granted_at: chrono::Utc::now(), - 18
expires_at: None, - 19
spend_limit_usd: Some(5.0), - 20
path_scope: vec!["src/**".into()], - 21
tool_scope: vec!["edit".into()], - 22
permission_ceiling: ceiling, - 23
escalation: Escalation::WaitIndefinitely, - 24
revoked_at: None, - 25
} - 26
} - 27
- 28
fn resolved(text: &str, authority: &Authority) -> vak_intent::Intent { - 29
let request = vak_intent::Request { - 30
text, - 31
turn_id: "0192f5a0-0000-7000-8000-000000000000", - 32
surface: vak_intent::Surface::Cli, - 33
..vak_intent::Request::default() - 34
}; - 35
vak_intent::resolve( - 36
&request, - 37
&vak_intent::Declared::default(), - 38
authority, - 39
&vak_intent::ResolverConfig::default(), - 40
) - 41
.intent() - 42
} - 43
- 44
/// The single most important property: no delegation, however broad, lets an - 45
/// irreversible action past without a human — not at the turn's ceiling, and - 46
/// not after a grant has narrowed its strands. - 47
#[test] - 48
fn no_grant_lets_irreversible_work_past_a_human() { - 49
let now = chrono::Utc::now(); - 50
for autonomy in Autonomy::ALL { - 51
let authority = Authority { - 52
autonomy, - 53
attendance: vak_intent::Attendance::Interactive, - 54
}; - 55
assert_eq!( - 56
authority.approval_ceiling(Stakes::Irreversible), - 57
ApprovalCeiling::Ask, - 58
"{autonomy:?}" - 59
); - 60
let intent = resolved("force push to the production branch", &authority); - 61
for ceiling in PermissionCeiling::ALL { - 62
let envelopes = intent - 63
.strands - 64
.iter() - 65
.map(|strand| (strand.strand_id.clone(), envelope(ceiling))) - 66
.collect(); - 67
let narrowed = vak_intent::apply_envelopes(intent.clone(), &envelopes, autonomy, now); - 68
assert_eq!( - 69
narrowed.engagement.limits.approval_ceiling, - 70
ApprovalCeiling::Ask, - 71
"{autonomy:?}/{ceiling:?}" - 72
); - 73
assert_ne!( - 74
narrowed.engagement.posture.hil, - 75
vak_intent::HilMode::Envelope, - 76
"irreversible work never proceeds inside an envelope" - 77
); - 78
} - 79
} - 80
} - 81
- 82
/// A grant only ever narrows the turn it applies to. - 83
#[test] - 84
fn a_grant_narrows_and_never_widens_the_turn() { - 85
let now = chrono::Utc::now(); - 86
let authority = Authority { - 87
autonomy: Autonomy::Delegated, - 88
attendance: vak_intent::Attendance::Interactive, - 89
}; - 90
let intent = resolved("refactor the parser module", &authority); - 91
for ceiling in PermissionCeiling::ALL { - 92
let envelopes = intent - 93
.strands - 94
.iter() - 95
.map(|strand| (strand.strand_id.clone(), envelope(ceiling))) - 96
.collect(); - 97
let narrowed = - 98
vak_intent::apply_envelopes(intent.clone(), &envelopes, Autonomy::Delegated, now); - 99
assert!( - 100
narrowed - 101
.engagement - 102
.limits - 103
.is_at_most(&intent.engagement.limits), - 104
"{ceiling:?} widened the turn" - 105
); - 106
assert_eq!(narrowed.engagement.limits.spend_ceiling_usd, Some(5.0)); - 107
} - 108
} - 109
- 110
/// A grant may lower the effective permission mode and may never raise it, - 111
/// composing through the same `capped_by` a gateway channel override uses. - 112
#[test] - 113
fn a_grant_can_only_lower_the_permission_mode() { - 114
use vak_config::PermissionMode::*; - 115
for configured in [ReadOnly, WorkspaceWrite, FullAccess] { - 116
for ceiling in PermissionCeiling::ALL { - 117
let effective = intent::permission_mode(configured, ceiling); - 118
assert!( - 119
effective.rank() <= configured.rank(), - 120
"{configured:?} + {ceiling:?} widened to {effective:?}" - 121
); - 122
} - 123
} - 124
// Specifically: a full-access grant does not promote a read-only workspace. - 125
assert_eq!( - 126
intent::permission_mode(ReadOnly, PermissionCeiling::FullAccess), - 127
ReadOnly - 128
); - 129
} - 130
- 131
/// Approval composition takes the stricter of configuration and the - 132
/// engagement's ceiling, in both directions. - 133
#[test] - 134
fn approval_composition_never_loosens_configuration() { - 135
use vak_config::ApprovalMode::*; - 136
for configured in [Ask, ApproveSafe, AutoApprove] { - 137
for ceiling in ApprovalCeiling::ALL { - 138
let effective = intent::approval_mode(configured, ceiling, true); - 139
let rank = |mode: vak_config::ApprovalMode| match mode { - 140
Ask => 0u8, - 141
ApproveSafe => 1, - 142
AutoApprove => 2, - 143
}; - 144
assert!(rank(effective) <= rank(configured)); - 145
} - 146
} - 147
} - 148
- 149
/// A reading decides only what is *loaded*: the surface partitions exactly the - 150
/// admitted tools into loaded and deferred, and can neither add a tool nor - 151
/// lose one. - 152
#[test] - 153
fn a_reading_partitions_the_admitted_tools_and_never_adds_or_drops_one() { - 154
let admitted = vak_tools::default_tools(); - 155
let names = |defs: &[vak_llm::ToolDefinition]| -> Vec<String> { - 156
defs.iter().map(|d| d.name.clone()).collect() - 157
}; - 158
for required in [ - 159
DomainSet::only(["code-exec"]), - 160
DomainSet::Empty, - 161
DomainSet::All, - 162
] { - 163
let surface = - 164
vak_core::capability::build_tool_surface(&admitted, &required, &Default::default()); - 165
let mut seen = names(&surface.core); - 166
seen.extend(names(&surface.deferred)); - 167
seen.sort(); - 168
let mut expected: Vec<String> = admitted.iter().map(|t| t.name().to_string()).collect(); - 169
expected.sort(); - 170
assert_eq!(seen, expected, "{required:?}"); - 171
assert!( - 172
names(&surface.core).contains(&"read".to_string()), - 173
"an always-loaded tool is loaded whatever the reading" - 174
); - 175
} - 176
} - 177
- 178
/// A revoked grant stops narrowing and does not leave a remembered widening - 179
/// behind, and delegation alone buys nothing at the turn level: the gate lets - 180
/// through only what a live grant covers, action by action. - 181
#[test] - 182
fn a_revoked_grant_grants_nothing() { - 183
let now = chrono::Utc::now(); - 184
let authority = Authority { - 185
autonomy: Autonomy::Delegated, - 186
attendance: vak_intent::Attendance::Supervised, - 187
}; - 188
assert_eq!( - 189
authority.approval_ceiling(Stakes::Reversible), - 190
ApprovalCeiling::Ask - 191
); - 192
let intent = resolved("refactor the parser module", &authority); - 193
let mut revoked = envelope(PermissionCeiling::ReadOnly); - 194
revoked.revoked_at = Some(now); - 195
let envelopes = intent - 196
.strands - 197
.iter() - 198
.map(|strand| (strand.strand_id.clone(), revoked.clone())) - 199
.collect(); - 200
assert_eq!( - 201
vak_intent::apply_envelopes(intent.clone(), &envelopes, Autonomy::Delegated, now), - 202
intent, - 203
"a revoked grant must impose no ceiling, and confer nothing either" - 204
); - 205
} - 206
- 207
/// Assuming a default for irreversible work because nobody replied is exactly - 208
/// the autonomy the system exists to prevent. - 209
#[test] - 210
fn a_silent_default_is_refused_for_irreversible_work() { - 211
let policy = Escalation::AssumeConservative { after_hours: 24 }; - 212
assert!(policy.permitted_for(Stakes::Reversible)); - 213
assert!(policy.permitted_for(Stakes::Costly)); - 214
assert!(!policy.permitted_for(Stakes::Irreversible)); - 215
} - 216
- 217
/// `vak-config` ranks autonomy names without depending on the intent kernel, - 218
/// so the two rankings must agree. This test sees both and is the only place - 219
/// that can check it. - 220
#[test] - 221
fn config_and_kernel_agree_on_autonomy_ranking() { - 222
for (lower, higher) in [ - 223
("manual", "assisted"), - 224
("assisted", "delegated"), - 225
("delegated", "autonomous"), - 226
] { - 227
let a = Autonomy::parse(lower).unwrap(); - 228
let b = Autonomy::parse(higher).unwrap(); - 229
assert!(a.rank() < b.rank()); - 230
// And the config-side merge must pick the same "less delegated" one. - 231
let capped = vak_config::ChannelPolicy::cap_autonomy(Some(higher), Some(lower)); - 232
assert_eq!(capped.as_deref(), Some(lower)); - 233
} - 234
} - 235
- 236
/// A channel ceiling composes downward with the workspace grant. - 237
#[test] - 238
fn a_channel_ceiling_caps_the_workspace_grant() { - 239
for granted in Autonomy::ALL { - 240
for ceiling in Autonomy::ALL { - 241
let effective = granted.capped_by(ceiling); - 242
assert!(effective.rank() <= granted.rank()); - 243
assert!(effective.rank() <= ceiling.rank()); - 244
} - 245
} - 246
} - 247
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.