Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/agent-spec/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ fn is_declaration_parent(catalog_root: &Path, dir: &Path) -> bool {
/// of [`SPEC_EXTS`]. `pty` session metadata includes JSON that can resemble an agent spec; it is
/// runner state, never catalog input. Unreadable directories are skipped, not fatal in ordinary
/// discovery. Strict discovery records them as uncertainty.
///
/// A declaration directory holds exactly one agent, so its subdirectories are that agent's own
/// state — evidence artifacts, receipts, scratch — and never more catalog. Descending into them
/// let an arbitrary JSON file become a spec: a malformed one failed the whole catalog, and a
/// well-formed one with an `argv` field silently became a launchable phantom agent.
fn collect_spec_files(
root: &Path,
dir: &Path,
Expand All @@ -230,6 +235,9 @@ fn collect_spec_files(
return;
}
};
// The catalog root is never an agent's own directory, even when a root envelope declares one
// inline, so `agents/` and its siblings below the root stay discoverable.
let holds_a_declaration = dir != root && is_declaration_parent(root, dir);
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Expand Down Expand Up @@ -263,6 +271,9 @@ fn collect_spec_files(
}
};
if ft.is_dir() {
if holds_a_declaration {
continue;
}
collect_spec_files(root, &path, acc, strict, errors);
} else if ft.is_file() && has_spec_extension(&path) {
acc.push(path);
Expand Down
94 changes: 94 additions & 0 deletions crates/agent-spec/tests/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,100 @@ fn explicit_json_null_fields_are_rejected_instead_of_granting_default_behavior()
}
}

#[test]
fn an_agents_own_subtree_is_state_and_never_another_declaration() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"agents/h/evidence.worker/agent.kdl",
"agent \"evidence.worker\" {\n host \"h\"\n argv \"true\"\n}\n",
);
write(
tmp.path(),
"agents/h/other.worker/agent.kdl",
"agent \"other.worker\" {\n host \"h\"\n argv \"true\"\n}\n",
);
// A declaration directory that is not a canonical `agents/<host>/<identity>` bundle. Nothing
// positional protects this one, so it is the case that still discriminates: only the rule that
// a declaration's subdirectories are its own state keeps the receipt below out of the catalog.
write(
tmp.path(),
"team/agent.kdl",
"agent \"team.worker\" {\n host \"h\"\n argv \"true\"\n}\n",
);
// Evidence artifacts an agent writes under its own directory. The first is well-formed and
// carries an `argv`, which used to lower into a launchable phantom agent with no error at
// all; the second is ordinary JSON that used to fail the whole catalog.
write(
tmp.path(),
"team/evidence/agent-launch-receipts/run-1.json",
r#"{"identity":"phantom.receipt","host":"h","argv":["true"]}"#,
);
write(
tmp.path(),
"team/evidence/agent-launch-receipts/run-2.json",
r#"[{"run":1},{"run":2}]"#,
);
// The same artifacts inside a canonical bundle, which `is_canonical_bundle_descendant` also
// covers positionally. Kept so the bundle case stays guarded if that rule ever moves.
write(
tmp.path(),
"agents/h/evidence.worker/axe/agent-launch-receipts/run-1.json",
r#"{"schema":"axe.agent-launch-receipt.v3","argv":["axe","agent","launch"]}"#,
);
write(
tmp.path(),
"agents/h/evidence.worker/axe/agent-launch-receipts/run-2.json",
r#"[{"run":1},{"run":2}]"#,
);

for found in [discover(tmp.path()), discover_strict(tmp.path())] {
let identities: Vec<&str> = found
.specs
.iter()
.map(|spec| spec.identity.as_str())
.collect();
assert_eq!(
identities,
["evidence.worker", "other.worker", "team.worker"],
"an evidence artifact became a declaration"
);
assert!(found.errors.is_empty(), "{:?}", found.errors);
}
}

#[test]
fn a_root_envelope_declaration_does_not_hide_the_agents_subtree() {
let tmp = tempfile::tempdir().unwrap();
// A root `catalog.kdl` may declare an agent inline beside a profile. The root is still the
// catalog envelope, not that agent's own directory, so refusing to descend from it would drop
// every canonical bundle in the catalog.
write(
tmp.path(),
"catalog.kdl",
"profile \"dev.example.goal\" {\n wasm \"resolver.wasm\"\n}\nagent \"root-agent\" { command \"true\" }\n",
);
write(
tmp.path(),
"agents/h/real.worker/agent.kdl",
"agent \"real.worker\" {\n host \"h\"\n argv \"true\"\n}\n",
);

for found in [discover(tmp.path()), discover_strict(tmp.path())] {
let identities: Vec<&str> = found
.specs
.iter()
.map(|spec| spec.identity.as_str())
.collect();
assert_eq!(
identities,
["real.worker", "root-agent"],
"a root envelope declaration hid the agents subtree"
);
assert!(found.errors.is_empty(), "{:?}", found.errors);
}
}

fn write(root: &Path, rel: &str, contents: &str) {
let path = root.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
Expand Down
Loading