fix(assembler): reject unsubstituted {{placeholder}} patterns
Walks every string-valued manifest field after load and rejects values
containing `{{...}}` (conservative: requires both `{{` and `}}`). Catches
wizard bugs that would emit literal `{{MEMORY_PROJECT}}` into the
generated agent .md. Four unit tests cover role/memory_project/single-brace
accept/empty-accept cases.
This commit is contained in:
parent
f04c9f38da
commit
60f464c0bc
1 changed files with 115 additions and 0 deletions
|
|
@ -34,5 +34,120 @@ pub fn validate(m: &Manifest, blocks_dir: &Path) -> Result<(), String> {
|
|||
return Err("role must not be empty".into());
|
||||
}
|
||||
|
||||
check_no_placeholders(m)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject manifests that still carry `{{PLACEHOLDER}}` tokens — the wizard
|
||||
/// should have substituted them. Emitted literally into generated .md they
|
||||
/// produce broken agents. Matches `{{...}}` conservatively (not single braces).
|
||||
fn check_no_placeholders(m: &Manifest) -> Result<(), String> {
|
||||
let check = |field: &str, value: &str| -> Result<(), String> {
|
||||
if contains_placeholder(value) {
|
||||
Err(format!(
|
||||
"Unsubstituted template placeholder in field '{field}': {value}. Did the wizard skip a substitution?"
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
check("name", &m.name)?;
|
||||
check("description", &m.description)?;
|
||||
check("model", &m.model)?;
|
||||
check("role", &m.role)?;
|
||||
for (i, t) in m.tools.iter().enumerate() {
|
||||
check(&format!("tools[{i}]"), t)?;
|
||||
}
|
||||
for (i, b) in m.blocks.iter().enumerate() {
|
||||
check(&format!("blocks[{i}]"), b)?;
|
||||
}
|
||||
for (i, d) in m.domain_in.iter().enumerate() {
|
||||
check(&format!("domain_in[{i}]"), d)?;
|
||||
}
|
||||
for (i, d) in m.forbidden_domain.iter().enumerate() {
|
||||
check(&format!("forbidden_domain[{i}]"), d)?;
|
||||
}
|
||||
for (i, h) in m.handoff.iter().enumerate() {
|
||||
check(&format!("handoff[{i}].target"), &h.target)?;
|
||||
check(&format!("handoff[{i}].trigger"), &h.trigger)?;
|
||||
}
|
||||
for (i, o) in m.output_extra_fields.iter().enumerate() {
|
||||
check(&format!("output_extra_fields[{i}]"), o)?;
|
||||
}
|
||||
if let Some(v) = &m.memory_project {
|
||||
check("memory_project", v)?;
|
||||
}
|
||||
if let Some(v) = &m.project_claudemd {
|
||||
check("project_claudemd", v)?;
|
||||
}
|
||||
if let Some(r) = &m.references {
|
||||
for (i, e) in r.extra.iter().enumerate() {
|
||||
check(&format!("references.extra[{i}]"), e)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn contains_placeholder(s: &str) -> bool {
|
||||
// Look for a `{{` with a matching `}}` later in the same string.
|
||||
if let Some(start) = s.find("{{") {
|
||||
if s[start + 2..].contains("}}") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::manifest::{Handoff, Manifest};
|
||||
|
||||
fn base() -> Manifest {
|
||||
Manifest {
|
||||
name: "test".into(),
|
||||
description: "d".into(),
|
||||
tools: vec!["Read".into()],
|
||||
model: "opus".into(),
|
||||
role: "r".into(),
|
||||
blocks: vec!["baseline".into(), "evidence-grading".into(), "memory-protocol".into()],
|
||||
domain_in: vec!["x".into()],
|
||||
forbidden_domain: vec!["y".into()],
|
||||
handoff: vec![Handoff { target: "a".into(), trigger: "b".into() }],
|
||||
output_extra_fields: vec![],
|
||||
memory_project: None,
|
||||
project_claudemd: None,
|
||||
references: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_placeholder_in_memory_project() {
|
||||
let mut m = base();
|
||||
m.memory_project = Some("{{MEMORY_PROJECT}}".into());
|
||||
let err = check_no_placeholders(&m).unwrap_err();
|
||||
assert!(err.contains("memory_project"), "err = {err}");
|
||||
assert!(err.contains("{{MEMORY_PROJECT}}"), "err = {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_single_braces() {
|
||||
let mut m = base();
|
||||
m.description = "hello {world}".into();
|
||||
assert!(check_no_placeholders(&m).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_empty_manifest() {
|
||||
assert!(check_no_placeholders(&base()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_placeholder_in_role() {
|
||||
let mut m = base();
|
||||
m.role = "do {{THING}}".into();
|
||||
assert!(check_no_placeholders(&m).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue