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:
+164
-40
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user