fix(sandbox): discover agent-scoped RAG mixin sidecars

An agent-scoped RAG writes its config to <data>/agents/<agent>/<rag>.yaml, so
its sbx mixin sidecar lands beside it as <rag>.sbx-mixin.yaml. Discovery scanned
the agents directory only for a file named exactly sbx-mixin.yaml, and scanned
for suffixed sidecars only in the top-level rags directory, so a RAG attached
while an agent was active contributed no network allow rule and no credential to
the sandbox. The failure was silent: the sandbox launched and the RAG was simply
unreachable from inside it.

The two collectors differed only in the filename shape they matched, so they are
now one scan that takes the set of layouts to look for. The agents directory
asks for both its own sbx-mixin.yaml and the suffixed sidecars one level in,
which is the shape that was missing. Discovery order is unchanged, and it is
load-bearing: each mixin becomes a --kit in list order and later ones layer over
earlier ones, so the workspace mixin must stay last.
This commit is contained in:
2026-08-11 14:05:31 -06:00
parent 5e2b9c98ad
commit dc677a2529
+164 -40
View File
@@ -67,13 +67,16 @@ pub fn discover() -> Result<Vec<DiscoveredMixin>> {
push_if_exists(&mut out, paths::sbx_mixin_file())?; push_if_exists(&mut out, paths::sbx_mixin_file())?;
push_if_exists(&mut out, paths::global_tools_sbx_mixin_file())?; push_if_exists(&mut out, paths::global_tools_sbx_mixin_file())?;
for path in collect_subdir_mixins(&paths::functions_dir()) { for path in collect_mixins(&paths::functions_dir(), &[ScanMode::SubdirNamed]) {
out.push(read_mixin(path)?); out.push(read_mixin(path)?);
} }
for path in collect_subdir_mixins(&paths::agents_data_dir()) { for path in collect_mixins(
&paths::agents_data_dir(),
&[ScanMode::SubdirNamed, ScanMode::SubdirFlat],
) {
out.push(read_mixin(path)?); out.push(read_mixin(path)?);
} }
for path in collect_flat_mixins(&paths::rags_dir()) { for path in collect_mixins(&paths::rags_dir(), &[ScanMode::Flat]) {
out.push(read_mixin(path)?); out.push(read_mixin(path)?);
} }
@@ -160,27 +163,54 @@ fn read_mixin(path: PathBuf) -> Result<DiscoveredMixin> {
}) })
} }
fn collect_subdir_mixins(dir: &Path) -> Vec<PathBuf> { /// One on-disk layout a mixin scan can look for. A scan takes a set of these,
/// and each mode contributes only the shape it names.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScanMode {
/// `<dir>/*.sbx-mixin.yaml`
Flat,
/// `<dir>/*/sbx-mixin.yaml`
SubdirNamed,
/// `<dir>/*/*.sbx-mixin.yaml`
SubdirFlat,
}
/// Collects mixin paths under `dir` for every requested layout. Missing or
/// unreadable directories yield nothing rather than an error — these paths are
/// all optional on disk.
///
/// Order is deterministic: flat matches first (sorted by file name), then each
/// subdirectory in sorted order, contributing its named mixin before its
/// suffixed ones.
fn collect_mixins(dir: &Path, modes: &[ScanMode]) -> Vec<PathBuf> {
let mut result = Vec::new(); let mut result = Vec::new();
let Ok(rd) = read_dir(dir) else { return result };
let mut entries: Vec<_> = rd if modes.contains(&ScanMode::Flat) {
.flatten() result.extend(suffixed_mixins_in(dir));
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) }
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries { let named = modes.contains(&ScanMode::SubdirNamed);
let candidate = entry.path().join(SBX_MIXIN_FILE_NAME); let subdir_flat = modes.contains(&ScanMode::SubdirFlat);
if candidate.exists() { if !named && !subdir_flat {
result.push(candidate); return result;
}
for subdir in subdirs_of(dir) {
if named {
let candidate = subdir.join(SBX_MIXIN_FILE_NAME);
if candidate.exists() {
result.push(candidate);
}
}
if subdir_flat {
result.extend(suffixed_mixins_in(&subdir));
} }
} }
result result
} }
fn collect_flat_mixins(dir: &Path) -> Vec<PathBuf> { fn suffixed_mixins_in(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new(); let mut result = Vec::new();
let Ok(rd) = read_dir(dir) else { return result }; let Ok(rd) = read_dir(dir) else { return result };
@@ -195,10 +225,21 @@ fn collect_flat_mixins(dir: &Path) -> Vec<PathBuf> {
.collect(); .collect();
entries.sort_by_key(|e| e.file_name()); entries.sort_by_key(|e| e.file_name());
for entry in entries { result.extend(entries.into_iter().map(|e| e.path()));
result.push(entry.path()); result
} }
fn subdirs_of(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
let Ok(rd) = read_dir(dir) else { return result };
let mut entries: Vec<_> = rd
.flatten()
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.collect();
entries.sort_by_key(|e| e.file_name());
result.extend(entries.into_iter().map(|e| e.path()));
result result
} }
@@ -218,6 +259,13 @@ mod tests {
root root
} }
fn file_names(paths: &[PathBuf]) -> Vec<&str> {
paths
.iter()
.map(|p| p.file_name().unwrap().to_str().unwrap())
.collect()
}
#[test] #[test]
fn summarize_counts_installs_and_domains() { fn summarize_counts_installs_and_domains() {
let root = unique_root("sbx-mixin-counts"); let root = unique_root("sbx-mixin-counts");
@@ -301,7 +349,7 @@ network:
} }
#[test] #[test]
fn collect_subdir_mixins_sorts_and_skips_missing() { fn subdir_named_scan_sorts_and_skips_missing() {
let root = unique_root("sbx-mixin-subdirs"); let root = unique_root("sbx-mixin-subdirs");
for name in ["zebra", "apple", "no-mixin", "mango"] { for name in ["zebra", "apple", "no-mixin", "mango"] {
let dir = root.join(name); let dir = root.join(name);
@@ -311,7 +359,7 @@ network:
} }
} }
let found = collect_subdir_mixins(&root); let found = collect_mixins(&root, &[ScanMode::SubdirNamed]);
let names: Vec<String> = found let names: Vec<String> = found
.iter() .iter()
.map(|p| { .map(|p| {
@@ -329,9 +377,9 @@ network:
} }
#[test] #[test]
fn collect_subdir_mixins_returns_empty_for_missing_dir() { fn subdir_named_scan_returns_empty_for_missing_dir() {
let absent = env::temp_dir().join("coyote-definitely-not-here-xyz"); let absent = env::temp_dir().join("coyote-definitely-not-here-xyz");
let found = collect_subdir_mixins(&absent); let found = collect_mixins(&absent, &[ScanMode::SubdirNamed]);
assert!(found.is_empty()); assert!(found.is_empty());
} }
@@ -511,7 +559,7 @@ network:
} }
#[test] #[test]
fn collect_flat_mixins_matches_rag_sidecars_by_suffix() { fn flat_scan_matches_rag_sidecars_by_suffix() {
let root = unique_root("flat-mixins"); let root = unique_root("flat-mixins");
fs::write(root.join("company-docs.sbx-mixin.yaml"), "kind: mixin\n").unwrap(); fs::write(root.join("company-docs.sbx-mixin.yaml"), "kind: mixin\n").unwrap();
fs::write(root.join("alpha.sbx-mixin.yaml"), "kind: mixin\n").unwrap(); fs::write(root.join("alpha.sbx-mixin.yaml"), "kind: mixin\n").unwrap();
@@ -519,39 +567,115 @@ network:
fs::write(root.join("notes.yaml"), "driver: yaml\n").unwrap(); fs::write(root.join("notes.yaml"), "driver: yaml\n").unwrap();
fs::create_dir_all(root.join("decoy.sbx-mixin.yaml")).unwrap(); fs::create_dir_all(root.join("decoy.sbx-mixin.yaml")).unwrap();
let found = collect_flat_mixins(&root); let found = collect_mixins(&root, &[ScanMode::Flat]);
let names: Vec<_> = found
.iter()
.map(|p| p.file_name().unwrap().to_str().unwrap())
.collect();
assert_eq!( assert_eq!(
names, file_names(&found),
vec!["alpha.sbx-mixin.yaml", "company-docs.sbx-mixin.yaml"] vec!["alpha.sbx-mixin.yaml", "company-docs.sbx-mixin.yaml"]
); );
let _ = fs::remove_dir_all(&root); let _ = fs::remove_dir_all(&root);
} }
/// Why `collect_flat_mixins` had to be written: the existing collector walks /// Every scan site in `discover()` picks its modes assuming each mode owns
/// SUBDIRECTORIES for a file named exactly `sbx-mixin.yaml`, so it cannot see /// exactly one layout and nothing else. `agents_data_dir()` requests two
/// a flat sidecar. If this ever starts finding them, the new collector is /// modes at once, so an overlap would collect the same file twice and
/// redundant — but until then, removing it silently drops every RAG mixin. /// `create_sandbox` would pass it as two `--kit` flags.
#[test] #[test]
fn collect_subdir_mixins_cannot_see_flat_rag_sidecars() { fn each_scan_mode_owns_exactly_one_layout() {
let root = unique_root("flat-vs-subdir"); let root = unique_root("scan-mode-ownership");
fs::write(root.join("company-docs.sbx-mixin.yaml"), "kind: mixin\n").unwrap(); let agent = root.join("researcher");
fs::create_dir_all(&agent).unwrap();
let flat = root.join("company-docs.sbx-mixin.yaml");
let subdir_named = agent.join("sbx-mixin.yaml");
let subdir_flat = agent.join("handbook.sbx-mixin.yaml");
for path in [&flat, &subdir_named, &subdir_flat] {
fs::write(path, "kind: mixin\n").unwrap();
}
assert!(collect_subdir_mixins(&root).is_empty()); assert_eq!(collect_mixins(&root, &[ScanMode::Flat]), vec![flat.clone()]);
assert_eq!(collect_flat_mixins(&root).len(), 1); assert_eq!(
collect_mixins(&root, &[ScanMode::SubdirNamed]),
vec![subdir_named.clone()]
);
assert_eq!(
collect_mixins(&root, &[ScanMode::SubdirFlat]),
vec![subdir_flat.clone()]
);
let all = collect_mixins(
&root,
&[ScanMode::Flat, ScanMode::SubdirNamed, ScanMode::SubdirFlat],
);
assert_eq!(all, vec![flat, subdir_named, subdir_flat]);
let mut deduped = all.clone();
deduped.sort();
deduped.dedup();
assert_eq!(
deduped.len(),
all.len(),
"no mixin may be collected twice: {all:?}"
);
let _ = fs::remove_dir_all(&root); let _ = fs::remove_dir_all(&root);
} }
#[test] #[test]
fn collect_flat_mixins_tolerates_a_missing_directory() { fn flat_scan_tolerates_a_missing_directory() {
let root = unique_root("flat-missing"); let root = unique_root("flat-missing");
let absent = root.join("nope"); let absent = root.join("nope");
assert!(collect_flat_mixins(&absent).is_empty()); assert!(collect_mixins(&absent, &[ScanMode::Flat]).is_empty());
let _ = fs::remove_dir_all(&root);
}
/// `generate_rag_sbx_mixin` writes an agent-scoped RAG sidecar next to the
/// rag yaml, at `<agents>/<agent>/<rag>.sbx-mixin.yaml`. Before `SubdirFlat`
/// existed, nothing scanned that shape and attaching a Qdrant RAG from
/// inside an agent produced no network allow rule and no credential.
#[test]
fn agent_scoped_rag_sidecar_is_discovered() {
let root = unique_root("agent-scoped-rag");
let agent = root.join("researcher");
fs::create_dir_all(&agent).unwrap();
fs::write(agent.join("company-docs.sbx-mixin.yaml"), "kind: mixin\n").unwrap();
fs::write(agent.join("company-docs.yaml"), "driver: qdrant\n").unwrap();
let found = collect_mixins(&root, &[ScanMode::SubdirNamed, ScanMode::SubdirFlat]);
assert_eq!(found, vec![agent.join("company-docs.sbx-mixin.yaml")]);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn agent_level_mixin_and_rag_sidecars_are_both_discovered() {
let root = unique_root("agent-both-shapes");
let agent = root.join("researcher");
fs::create_dir_all(&agent).unwrap();
fs::write(agent.join("sbx-mixin.yaml"), "kind: mixin\n").unwrap();
fs::write(agent.join("zebra.sbx-mixin.yaml"), "kind: mixin\n").unwrap();
fs::write(agent.join("alpha.sbx-mixin.yaml"), "kind: mixin\n").unwrap();
let found = collect_mixins(&root, &[ScanMode::SubdirNamed, ScanMode::SubdirFlat]);
assert_eq!(
file_names(&found),
vec![
"sbx-mixin.yaml",
"alpha.sbx-mixin.yaml",
"zebra.sbx-mixin.yaml"
]
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn subdir_flat_scan_ignores_a_directory_named_like_a_mixin() {
let root = unique_root("subdir-flat-decoy");
let agent = root.join("researcher");
fs::create_dir_all(agent.join("decoy.sbx-mixin.yaml")).unwrap();
assert!(collect_mixins(&root, &[ScanMode::SubdirFlat]).is_empty());
let _ = fs::remove_dir_all(&root); let _ = fs::remove_dir_all(&root);
} }