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:
@@ -2,6 +2,31 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// JSON schema fragment for the `origin` argument required by every `create_*`
|
||||
/// tool (bug 1102). The caller MUST identify itself — empty `id` is rejected
|
||||
/// server-side so every work item carries a usable provenance trail.
|
||||
fn origin_schema() -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"description": "Required: identifies the calling actor so every work item carries provenance. Empty/missing id is rejected (bug 1102). Examples: { \"kind\": \"agent\", \"id\": \"coder-1@story=42\" }, { \"kind\": \"chat-bot\", \"id\": \"Timmy@!room:home.local\" }, { \"kind\": \"user\", \"id\": \"dave\" }.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "One of: \"agent\" (LLM coder/mergemaster/qa), \"chat-bot\" (Timmy or other chat-routed bot), \"user\" (human via CLI/MCP), \"system\" (server-automation)."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Non-empty identifier of the caller. For agents include the story id (e.g. \"coder-1@story=42\"); for chat-bots include the room/session (e.g. \"Timmy@!room:home.local\"); for users the user id or short name."
|
||||
},
|
||||
"ts": {
|
||||
"type": "number",
|
||||
"description": "Optional unix-seconds timestamp. Defaults to the server's clock when absent."
|
||||
}
|
||||
},
|
||||
"required": ["kind", "id"]
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns tool schemas for story/work-item lifecycle management.
|
||||
pub(super) fn story_tools() -> Vec<Value> {
|
||||
vec![
|
||||
@@ -37,9 +62,10 @@ pub(super) fn story_tools() -> Vec<Value> {
|
||||
"commit": {
|
||||
"type": "boolean",
|
||||
"description": "If true, git-add and git-commit the new story file to the current branch"
|
||||
}
|
||||
},
|
||||
"origin": origin_schema()
|
||||
},
|
||||
"required": ["name", "acceptance_criteria"]
|
||||
"required": ["name", "acceptance_criteria", "origin"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
@@ -282,9 +308,10 @@ pub(super) fn story_tools() -> Vec<Value> {
|
||||
"items": { "type": "string" },
|
||||
"minItems": 1,
|
||||
"description": "List of acceptance criteria (at least one required)"
|
||||
}
|
||||
},
|
||||
"origin": origin_schema()
|
||||
},
|
||||
"required": ["name", "acceptance_criteria"]
|
||||
"required": ["name", "acceptance_criteria", "origin"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
@@ -323,9 +350,10 @@ pub(super) fn story_tools() -> Vec<Value> {
|
||||
"type": "array",
|
||||
"items": { "type": "integer" },
|
||||
"description": "Optional list of story numbers this bug depends on (e.g. [42, 43]). Persisted as depends_on in YAML front matter."
|
||||
}
|
||||
},
|
||||
"origin": origin_schema()
|
||||
},
|
||||
"required": ["name", "description", "steps_to_reproduce", "actual_result", "expected_result", "acceptance_criteria"]
|
||||
"required": ["name", "description", "steps_to_reproduce", "actual_result", "expected_result", "acceptance_criteria", "origin"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
@@ -360,9 +388,10 @@ pub(super) fn story_tools() -> Vec<Value> {
|
||||
"type": "array",
|
||||
"items": { "type": "integer" },
|
||||
"description": "Optional list of story numbers this refactor depends on (e.g. [42, 43]). Persisted as depends_on in YAML front matter."
|
||||
}
|
||||
},
|
||||
"origin": origin_schema()
|
||||
},
|
||||
"required": ["name", "acceptance_criteria"]
|
||||
"required": ["name", "acceptance_criteria", "origin"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
@@ -399,9 +428,10 @@ pub(super) fn story_tools() -> Vec<Value> {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Optional: list of high-level success criteria for the epic"
|
||||
}
|
||||
},
|
||||
"origin": origin_schema()
|
||||
},
|
||||
"required": ["name", "goal"]
|
||||
"required": ["name", "goal", "origin"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
|
||||
Reference in New Issue
Block a user