fix(1102): require non-empty origin.id on create_* MCP tools

bug 1102 was created today with origin={kind:user, id:""} because
build_origin silently defaulted id to empty when the caller didn't pass
one — we couldn't tell who filed it. Bug 1088's origin field is useless
as audit if every caller can omit themselves.

Changes:
- build_origin (server/src/http/mcp/story_tools/mod.rs) now returns
  Result<String, String> and rejects missing/empty/whitespace-only id
  with an instructional error pointing at bug 1102 / story 1104.
- 5 create_* tool handlers (bug, spike, refactor, epic, story) now
  resolve origin BEFORE create_*_file so an attribution-less call
  leaves no half-state behind.
- 5 tool input schemas now advertise origin as a required object via
  a shared origin_schema() helper. The schema description gives every
  caller (coder agent, chat bot, user, system) a concrete example so
  the LLM populates the field correctly on first sight.
- Test fixtures pass origin = {kind:"test", id:"test-suite"}.

Story 1104 (signed actions) is the longer-term replacement; this is the
quick attribution win agreed for master ahead of that design work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-15 23:13:54 +01:00
parent 26527e7dae
commit 6fbe239313
8 changed files with 170 additions and 50 deletions
+12 -3
View File
@@ -13,6 +13,10 @@ use serde_json::{Value, json};
pub(crate) fn tool_create_epic(args: &Value, ctx: &AppContext) -> Result<String, String> {
let req = CreateEpicRequest::from_json(args)?;
// Bug 1102: resolve and validate origin BEFORE creating the epic so a
// missing-attribution call leaves no half-state behind.
let origin = super::build_origin(args)?;
let root = ctx.state.get_project_root()?;
let success_criteria = req.success_criteria_strings();
@@ -29,7 +33,7 @@ pub(crate) fn tool_create_epic(args: &Value, ctx: &AppContext) -> Result<String,
},
)?;
crate::crdt_state::set_origin(&epic_id, &super::build_origin(args));
crate::crdt_state::set_origin(&epic_id, &origin);
Ok(format!("Created epic: {epic_id}"))
}
@@ -170,7 +174,8 @@ mod tests {
"name": "My Test Epic",
"goal": "Achieve something great",
"motivation": "Because it matters",
"success_criteria": ["All stories done", "Tests pass"]
"success_criteria": ["All stories done", "Tests pass"],
"origin": {"kind": "test", "id": "test-suite"}
}),
&ctx,
);
@@ -223,7 +228,11 @@ mod tests {
// Create an epic.
tool_create_epic(
&json!({"name": "List Epics Test Epic", "goal": "Testing list"}),
&json!({
"name": "List Epics Test Epic",
"goal": "Testing list",
"origin": {"kind": "test", "id": "test-suite"}
}),
&ctx,
)
.unwrap();