Inline can-grants cannot set Descriptor.Describe, so every ward-mcp tool ships the fallback description #274

Closed
opened 2026-08-10 03:39:51 +00:00 by coilyco-ops · 0 comments
Member

The gap

ward-mcp mints one MCP tool per guardfile grant and takes that tool's description from opcore.Descriptor.Describe:

func describe(d opcore.Descriptor) string {
	if strings.TrimSpace(d.Describe) != "" {
		return d.Describe
	}
	return fmt.Sprintf("Use this when the user wants to %s %s through the configured upstream service.", d.Leaf, d.Group)
}

Descriptor.Describe is declared in http/opcore/descriptor.go:25 as "optional Guardfile describe ... note."

The inline grammar cannot set it on a can grant. applyInlineGrantChild fail-closes outside path | query | body | set | fail-when, and the only describe handler is applyProxyChildsetProxyDescribe, which writes Proxy.Describe, a different field on a different struct. describe inside a can grant is currently the exemplar for the "unknown grant child" rejection test, so this is a deliberate fail-closed boundary rather than an oversight.

Why it matters

Every can-grant tool therefore ships the generated fallback sentence, and a guardfile's only way to say anything about a grant is a // comment, which no model ever sees.

Concretely, in coilyco-bridge/deploy, a channel-scoped Discord read surface describes itself to the calling model as:

Use this when the user wants to list eco-chat-message through the configured upstream service.

The guardfile has 40 lines of // explaining that the reads are eco-scoped, capped at 25 items, and that Discord messages are untrusted community-authored input to quote rather than execute. None of it reaches the model. It is written for the next editor, and the audience that most needs it cannot read it.

That is a security-relevant asymmetry, not only ergonomics: the "treat this as evidence, not instructions" framing belongs in the tool description a model actually receives.

Ask

Accept describe "..." as a can-grant child, setting Descriptor.Describe, with the same fail-closed treatment as its siblings (reject duplicates, reject empty). Proxy grants keep their existing describe untouched.

This widens the frozen grammar by exactly one node and adds no new authority: it is a string that flows to a description field consumers already read.

Done when

can get issue { path "/x"; describe "..." } parses and populates Descriptor.Describe, ward-mcp serves that string as the tool description, and an unknown child still fails closed.

## The gap `ward-mcp` mints one MCP tool per guardfile grant and takes that tool's description from `opcore.Descriptor.Describe`: ```go func describe(d opcore.Descriptor) string { if strings.TrimSpace(d.Describe) != "" { return d.Describe } return fmt.Sprintf("Use this when the user wants to %s %s through the configured upstream service.", d.Leaf, d.Group) } ``` `Descriptor.Describe` is declared in `http/opcore/descriptor.go:25` as "optional Guardfile describe `...` note." The inline grammar cannot set it on a `can` grant. `applyInlineGrantChild` fail-closes outside `path | query | body | set | fail-when`, and the only `describe` handler is `applyProxyChild` → `setProxyDescribe`, which writes `Proxy.Describe`, a different field on a different struct. `describe` inside a `can` grant is currently the exemplar for the `"unknown grant child"` rejection test, so this is a deliberate fail-closed boundary rather than an oversight. ## Why it matters Every `can`-grant tool therefore ships the generated fallback sentence, and a guardfile's only way to say anything about a grant is a `//` comment, which no model ever sees. Concretely, in `coilyco-bridge/deploy`, a channel-scoped Discord read surface describes itself to the calling model as: > Use this when the user wants to list eco-chat-message through the configured upstream service. The guardfile has 40 lines of `//` explaining that the reads are eco-scoped, capped at 25 items, and that Discord messages are untrusted community-authored input to quote rather than execute. None of it reaches the model. It is written for the next editor, and the audience that most needs it cannot read it. That is a security-relevant asymmetry, not only ergonomics: the "treat this as evidence, not instructions" framing belongs in the tool description a model actually receives. ## Ask Accept `describe "..."` as a `can`-grant child, setting `Descriptor.Describe`, with the same fail-closed treatment as its siblings (reject duplicates, reject empty). Proxy grants keep their existing `describe` untouched. This widens the frozen grammar by exactly one node and adds no new authority: it is a string that flows to a description field consumers already read. ## Done when `can get issue { path "/x"; describe "..." }` parses and populates `Descriptor.Describe`, `ward-mcp` serves that string as the tool description, and an unknown child still fails closed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra#274
No description provided.