- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! Writes to the Shared configuration layer (`PATCH /config/global`). - 4
//! - 5
//! That layer is `vak_config::global_path()`, resolved from the process-wide - 6
//! home, and every `Core::new` in the process reads it. These tests write it, - 7
//! so they cannot share a binary with tests that only read it. They used to: - 8
//! a reader there ran under whatever permission mode a writer had just - 9
//! persisted, and one caught between `load_with_trust`'s existence check and - 10
//! its read of the file a writer was removing failed outright. Here the - 11
//! tests take turns, each on a private, empty home. - 12
- 13
use axum::{ - 14
Router, - 15
body::Body, - 16
http::{Request, StatusCode}, - 17
}; - 18
use http_body_util::BodyExt; - 19
use serde_json::{Value, json}; - 20
use tower::ServiceExt; - 21
use vak_core::Core; - 22
- 23
/// A private, empty home pinned for one test. The home is process state, so - 24
/// holding one also holds the turn: two tests here never overlap. - 25
struct PrivateHome { - 26
_root: tempfile::TempDir, - 27
_turn: std::sync::MutexGuard<'static, ()>, - 28
} - 29
- 30
fn private_home() -> PrivateHome { - 31
static TURN: std::sync::Mutex<()> = std::sync::Mutex::new(()); - 32
let turn = TURN - 33
.lock() - 34
.unwrap_or_else(std::sync::PoisonError::into_inner); - 35
let root = tempfile::tempdir().unwrap(); - 36
vak_config::paths::set_home_override(root.path()); - 37
PrivateHome { - 38
_root: root, - 39
_turn: turn, - 40
} - 41
} - 42
- 43
async fn patch_global(app: &Router, body: Value) -> (StatusCode, Value) { - 44
let response = app - 45
.clone() - 46
.oneshot( - 47
Request::builder() - 48
.method("PATCH") - 49
.uri("/config/global") - 50
.header("content-type", "application/json") - 51
.body(Body::from(body.to_string())) - 52
.unwrap(), - 53
) - 54
.await - 55
.unwrap(); - 56
let status = response.status(); - 57
let bytes = response.into_body().collect().await.unwrap().to_bytes(); - 58
( - 59
status, - 60
serde_json::from_slice(&bytes).unwrap_or(Value::Null), - 61
) - 62
} - 63
- 64
/// The gateway's own workspace IS the default workspace, so one file - 65
/// serves as both layers and `load_with_trust` skips the project pass. - 66
/// Reporting it as "shadowed" told an operator their change would not - 67
/// take effect when it would — observed live, on a real install, right - 68
/// after the shadow check shipped. - 69
#[tokio::test] - 70
async fn a_global_write_on_the_default_workspace_is_not_its_own_shadow() { - 71
let _home = private_home(); - 72
let global = vak_config::global_path().unwrap(); - 73
let workspace = vak_config::paths::default_workspace(); - 74
std::fs::create_dir_all(global.parent().expect("parent")).unwrap(); - 75
std::fs::write(&global, "permission_mode = \"read-only\"\n").unwrap(); - 76
vak_core::trust::record(&workspace).unwrap(); - 77
- 78
let core = Core::new_with_trust(workspace.clone(), true).unwrap(); - 79
core.set_sessions_home(workspace.join(".sessions")); - 80
assert_eq!( - 81
vak_config::project_path(core.cwd()), - 82
global, - 83
"this test is only meaningful when the two layers are one file" - 84
); - 85
let app = vak_server::router(core.clone()); - 86
- 87
let (status, json) = patch_global(&app, json!({ "permission_mode": "workspace-write" })).await; - 88
assert_eq!(status, StatusCode::OK); - 89
assert_eq!( - 90
json["shadowed_by_project"], - 91
json!([]), - 92
"one file cannot shadow itself: {json}" - 93
); - 94
assert_eq!( - 95
core.effective_permission_mode(), - 96
vak_config::PermissionMode::WorkspaceWrite, - 97
"and the change must actually be in force" - 98
); - 99
} - 100
- 101
/// The project layer merges last, so a project pin wins. Applying the - 102
/// global value anyway made the running process disagree with what the - 103
/// files resolve to — until a restart put it back, which read as the - 104
/// operator's change being forgotten. - 105
#[tokio::test] - 106
async fn a_global_write_under_a_project_pin_persists_without_taking_effect() { - 107
let _home = private_home(); - 108
let dir = tempfile::tempdir().unwrap(); - 109
std::fs::create_dir_all(dir.path().join(".vak")).unwrap(); - 110
std::fs::write( - 111
dir.path().join(".vak/config.toml"), - 112
"permission_mode = \"read-only\"\n", - 113
) - 114
.unwrap(); - 115
vak_core::trust::record(dir.path()).unwrap(); - 116
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 117
core.set_sessions_home(dir.path().join("home")); - 118
assert_eq!( - 119
core.effective_permission_mode(), - 120
vak_config::PermissionMode::ReadOnly - 121
); - 122
let app = vak_server::router(core.clone()); - 123
- 124
let (status, _) = patch_global(&app, json!({ "permission_mode": "full-access" })).await; - 125
assert_eq!(status, StatusCode::OK); - 126
- 127
assert_eq!( - 128
core.effective_permission_mode(), - 129
vak_config::PermissionMode::ReadOnly, - 130
"the project pin still decides what this process runs at" - 131
); - 132
let global = vak_config::load_with_trust(dir.path(), true).unwrap(); - 133
assert_eq!( - 134
global.permission_mode, - 135
vak_config::PermissionMode::ReadOnly, - 136
"and what the files resolve to agrees" - 137
); - 138
} - 139
- 140
fn install_retired_plugin(root: &std::path::Path, name: &str, scope: vak_plugin::InstallScope) { - 141
let staging = tempfile::tempdir().unwrap(); - 142
let package = staging.path().join(name); - 143
std::fs::create_dir_all(package.join("skills/legacy")).unwrap(); - 144
std::fs::write( - 145
package.join("vak-plugin.json"), - 146
format!( - 147
r#"{{"schema":1,"name":"{name}","version":"1.0.0","description":"Old.","license":"MIT","components":{{"skills":["skills"]}}}}"# - 148
), - 149
) - 150
.unwrap(); - 151
std::fs::write( - 152
package.join("skills/legacy/SKILL.md"), - 153
"---\nname: legacy\ndescription: Old.\n---\n\nCall `python_eval`.\n", - 154
) - 155
.unwrap(); - 156
vak_plugin::PluginStore::new(root) - 157
.install_local( - 158
&package, - 159
vak_plugin::InstallOptions { - 160
scope, - 161
allow_unlicensed: false, - 162
}, - 163
) - 164
.unwrap(); - 165
} - 166
- 167
fn network_allow(path: &std::path::Path) -> Value { - 168
let config: toml::Value = toml::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); - 169
serde_json::to_value(&config["plugins"]["network_allow"]).unwrap() - 170
} - 171
- 172
/// A user-scope plugin's grant lives in the Shared layer and a workspace - 173
/// plugin's in the project layer; removing either prunes its own layer. - 174
#[tokio::test] - 175
async fn removing_retired_plugins_prunes_the_layer_of_each_scope() { - 176
let _home = private_home(); - 177
let dir = tempfile::tempdir().unwrap(); - 178
let global = vak_config::global_path().unwrap(); - 179
let project = vak_config::project_path(dir.path()); - 180
install_retired_plugin( - 181
global.parent().unwrap(), - 182
"user-eval", - 183
vak_plugin::InstallScope::User, - 184
); - 185
install_retired_plugin( - 186
project.parent().unwrap(), - 187
"project-eval", - 188
vak_plugin::InstallScope::Workspace, - 189
); - 190
let grants = "[plugins]\nnetwork_allow = [\"user-eval\", \"project-eval\", \"kept\"]\n"; - 191
std::fs::write(&global, grants).unwrap(); - 192
std::fs::write(&project, grants).unwrap(); - 193
vak_core::trust::record(dir.path()).unwrap(); - 194
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 195
core.set_sessions_home(dir.path().join("home")); - 196
let app = vak_server::router(core); - 197
- 198
let response = app - 199
.oneshot( - 200
Request::builder() - 201
.method("DELETE") - 202
.uri("/plugins/retired") - 203
.body(Body::empty()) - 204
.unwrap(), - 205
) - 206
.await - 207
.unwrap(); - 208
assert_eq!(response.status(), StatusCode::OK); - 209
- 210
assert_eq!(network_allow(&global), json!(["project-eval", "kept"])); - 211
assert_eq!(network_allow(&project), json!(["user-eval", "kept"])); - 212
} - 213
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.