- 1186
}))) - 1187
} - 1188
- 1189
/// DELETE /feeds/alerts/{name} — Remove an alert rule from config. - 1190
pub async fn delete_feed_alert( - 1191
Path(name): Path<String>, - 1192
Query(params): Query<HashMap<String, String>>, - 1193
State(state): State<AppState>, - 1194
) -> Result<impl IntoResponse, (StatusCode, String)> { - 1195
let cwd = state.core.cwd(); - 1196
let scope = params - 1197
.get("scope") - 1198
.map(String::as_str) - 1199
.unwrap_or("workspace"); - 1200
authorize_feed_mutation(&state, scope)?; - 1201
let config_path = if scope == "global" { - 1202
global_feeds_config_path() - 1203
} else { - 1204
feeds_config_path(cwd) - 1205
}; - 1206
- 1207
let alert = name.clone(); - 1208
edit_feeds_config(config_path.clone(), move |current| { - 1209
let content = - 1210
current.ok_or_else(|| (StatusCode::NOT_FOUND, "Config file not found".to_string()))?; - 1211
let next = remove_array_table_block(content, "alerts", &alert) - 1212
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Alert '{alert}' not found")))?; - 1213
Ok((Some(next), ())) - 1214
}) - 1215
.await?; - 1216
run_feed_script(cwd, "feed_ingest.py", &["--sync-alerts"]).await?; - 1217
- 1218
Ok(Json(json!({ - 1219
"status": "ok", - 1220
"message": format!("Alert '{}' removed from {}", name, config_path.display()), - 1221
}))) - 1222
} - 1223
- 1224
/// Build feed routes. - 1225
pub fn routes() -> Router<AppState> { - 1226
Router::new() - 1227
.route( - 1228
"/feeds/sources", - 1229
get(list_source_types).post(add_feed_source), - 1230
) - 1231
.route("/feeds/sources/configured", get(list_configured_sources)) - 1232
.route( - 1233
"/feeds/sources/{name}", - 1234
delete(delete_feed_source).patch(update_feed_source), - 1235
) - 1236
.route("/feeds/config", get(get_feed_config)) - 1237
.route("/feeds/items", get(list_feed_items)) - 1238
.route("/feeds/items/{id}", get(get_feed_item)) - 1239
.route("/feeds/search", get(search_feed_items)) - 1240
.route("/feeds/stats", get(get_feed_stats)) - 1241
.route("/feeds/alerts", get(get_feed_alerts).post(add_feed_alert)) - 1242
.route("/feeds/runs", get(get_feed_runs)) - 1243
.route("/feeds/quarantine", get(get_feed_quarantine)) - 1244
.route("/feeds/quarantine/{id}/release", post(release_feed_item)) - 1245
.route("/feeds/alerts/{name}", delete(delete_feed_alert)) - 1246
.route("/feeds/ingest", post(trigger_ingestion)) - 1247
} - 1248
- 1249
#[cfg(test)] - 1250
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)] - 1251
mod tests { - 1252
use super::{authorize_feed_scope, feed_environment, feeds_dir, validate_source_url}; - 1253
use vak_config::PermissionMode; - 1254
- 1255
/// The feed subprocess runs with `env_clear`, so `feed_environment` is the - 1256
/// complete list of what it sees. It must carry the operational minimum - 1257
/// (PATH, so `python3` resolves) and never a parent secret — before this, - 1258
/// the subprocess inherited the whole server environment, `VAK_GATEWAY_TOKEN` - 1259
/// and provider keys included (invariant 12). - 1260
#[test] - 1261
fn feed_subprocess_environment_is_a_secret_free_allowlist() { - 1262
let _home = vak_config::paths::isolate_home_for_tests(); - 1263
let environment = feed_environment( - 1264
std::path::Path::new("/tmp/ws"), - 1265
std::path::Path::new("/tmp/scripts"), - 1266
); - 1267
let allowed = [ - 1268
"PYTHONPATH", - 1269
"VAK_FEED_WORKSPACE", - 1270
"VAK_FEEDS_DB", - 1271
"VAK_FEEDS_LOG", - 1272
"VAK_FEEDS_CONFIG", - 1273
"PATH", - 1274
"HOME", - 1275
]; - 1276
for (key, _) in &environment { - 1277
assert!(allowed.contains(key), "{key} is not in the feed allowlist"); - 1278
} - 1279
assert!( - 1280
environment.iter().any(|(key, _)| *key == "PATH"), - 1281
"PATH must be passed so python3 resolves under env_clear" - 1282
); - 1283
for secret in ["VAK_GATEWAY_TOKEN", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"] { - 1284
assert!( - 1285
!environment.iter().any(|(key, _)| *key == secret), - 1286
"{secret} must never be passed to a feed subprocess" - 1287
); - 1288
} - 1289
} - 1290
- 1291
/// The script the server runs is located only from the binary, never from - 1292
/// the session workspace: a workspace with its own `scripts/feeds` cannot - 1293
/// supply the code the server executes (invariants 12, 14, 15). - 1294
#[test] - 1295
fn feeds_dir_never_resolves_from_a_workspace() { - 1296
let workspace = tempfile::tempdir().expect("tempdir"); - 1297
let planted = workspace.path().join("scripts").join("feeds"); - 1298
std::fs::create_dir_all(&planted).expect("mkdir"); - 1299
std::fs::write(planted.join("feed_ingest.py"), b"raise SystemExit\n").expect("write"); - 1300
assert!( - 1301
!feeds_dir().starts_with(workspace.path()), - 1302
"feeds_dir resolved a script from the session workspace" - 1303
); - 1304
} - 1305
- 1306
/// Every path a feed script writes comes from the canonical data home, - 1307
/// so an overridden `VAK_HOME` holds the feed store, its log and its - 1308
/// config; the scripts used to work out the platform default for - 1309
/// themselves and wrote outside it. - 1310
#[test] - 1311
fn feeds_write_under_overridden_home() { - 1312
let home = vak_config::paths::isolate_home_for_tests(); - 1313
let environment = feed_environment( - 1314
std::path::Path::new("/tmp/ws"), - 1315
std::path::Path::new("/tmp/scripts"), - 1316
); - 1317
for name in ["VAK_FEEDS_DB", "VAK_FEEDS_LOG", "VAK_FEEDS_CONFIG"] { - 1318
let (_, value) = environment - 1319
.iter() - 1320
.find(|(key, _)| *key == name) - 1321
.unwrap_or_else(|| panic!("{name} is passed")); - 1322
assert!( - 1323
std::path::Path::new(value).starts_with(&home), - 1324
"{name} = {value} is outside {}", - 1325
home.display() - 1326
); - 1327
} - 1328
} - 1329
- 1330
#[test] - 1331
fn feed_scope_permissions_only_tighten() { - 1332
assert!(authorize_feed_scope("workspace", PermissionMode::WorkspaceWrite).is_ok()); - 1333
assert!(authorize_feed_scope("workspace", PermissionMode::FullAccess).is_ok()); - 1334
assert!(authorize_feed_scope("global", PermissionMode::FullAccess).is_ok()); - 1335
assert!(authorize_feed_scope("global", PermissionMode::WorkspaceWrite).is_err()); - 1336
assert!(authorize_feed_scope("workspace", PermissionMode::ReadOnly).is_err()); - 1337
assert!(authorize_feed_scope("global", PermissionMode::ReadOnly).is_err()); - 1338
} - 1339
- 1340
#[test] - 1341
fn source_admission_rejects_private_and_malformed_targets() { - 1342
assert!(validate_source_url("https://example.com/feed").is_ok()); - 1343
assert!(validate_source_url("not a url").is_err()); - 1344
assert!(validate_source_url("file:///tmp/feed").is_err()); - 1345
assert!(validate_source_url("http://127.0.0.1/feed").is_err()); - 1346
assert!(validate_source_url("http://192.168.1.10/feed").is_err()); - 1347
assert!(validate_source_url("http://[::1]/feed").is_err()); - 1348
} - 1349
} - 1350
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.