madt_finish over MCP ignores summary=@file and ships the literal "@path" as the merged PR body #792
Labels
No labels
breaking
bug
documentation
enhancement
epic
good first issue
help wanted
refactoring
resolution/duplicate
resolution/invalid
resolution/wontfix
security
severity/critical
severity/high
severity/low
severity/medium
status/abandoned
status/blocked
status/needs-decision
status/needs-info
status/needs-verification
testing
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sixfold-space/madtea#792
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happens
madt_finish(summary=@/path/to/file.md)over the MCP surface does not read the file. The server takes the value verbatim, so the PR body becomes the literal string@/path/to/file.md.madt_finishcommits, pushes, opens the PR, and merges in one call, so that broken body is already on a merged PR before I can look at it. The merge result says there is no need to re-verify, so nothing points at the loss.In the code:
@file:internal/cmd/workflow/finish.go:209-233.internal/mcp/tools_workflow.go:104passesSummary: input.Summarystraight through. There is no@handling anywhere ininternal/mcp/orinternal/service/for this field.internal/opspec/paramdocs.go:224("use @file to read from file, @- for stdin"), surfaced into the MCP help atinternal/mcp/help_generated.go:3318.So a caller that follows the parameter doc gets a merged PR with no real body.
Why it matters
Why not to read the file server-side
The obvious fix - have the MCP server read
@fileand use its content - opens a credential-exfiltration hole, so I will not take it. The value is caller-controlled. The server reads with its own file access, which is the same account that drives the tool. The content is then published to the forge. A value likesummary=@~/.ssh/id_ed25519or@<a token-bearing config file>would read a secret and publish it. Untrusted text processed upstream (an issue body, a diff, a fetched page) can steer that value. A human types CLI@filewith intent; the MCP surface has no such gate. Server-side file reads for forge text do not belong on the MCP surface.Fix
One shared refusal at the MCP argument-validation choke point, not a per-field patch:
@/,@~,@., or@-- for every text parameter (summary, body, title, comment, and any future one), so no field can reintroduce the pattern.@-(stdin) has no meaning over MCP; the same refusal covers it.@file/@-claim from thesummaryparameter doc on the MCP surface (internal/opspec/paramdocs.go:224). The CLI keeps the behavior; MCP will not.This follows the single-guard pattern already in the tree:
util.ReadStdinis the one shared guard for every explicit-/@-stdin read (internal/util/input.go:26).Scope
Today only
summarycarries the@filepromise over MCP. Bodies already take inline text over MCP, and--body-fileis CLI-only (not exposed over MCP). The shared guard is the whole point: it closes the class so a new text field cannot reopen it. CLI behavior (human-typed@file,@-,--body-file) is unchanged.Acceptance (test first)
finishhandler withsummary=@/etc/hostname(and@-,@~/x,@./x) and asserts an error with no push, no PR, and no merge - the guard fires before any forge effect. Add the same for the other text-param tools.summaryparameter doc no longer claims@fileor@-.