opcore's verb-to-method convention may not know reopen, which would silently emit POST #281

Closed
opened 2026-08-12 17:26:30 +00:00 by coilyco-ops · 1 comment
Member

Filed speculatively — verify before fixing

I have not read http/opcore/method.go. This is filed from the convention as documented in a consuming guardfile, because if it holds it is a footgun worth closing, and if it does not the issue costs one read to reject.

Refiled from coilyco-flight-deck/ward#1656, which I opened against the wrong repository. coilyco-bridge/deploy/services/sirens-echo/forgejo-mcp.mcp.kdl describes the grammar as "parsed by opcore.ParseInline from ward-mcp's pinned cli-guard" — so opcore and its method table are cli-guard's, and ward-mcp is a consumer that pins them. ward#1656 is closed pointing here.

The claim

coilyco-bridge/deploy/services/forgejo-mcp/forgejo.mcp.kdl documents the method convention in its header:

Method comes from the verb by convention (get/list/search/view -> GET, create/comment/add and unknown verbs such as pin -> POST, edit/close -> PATCH, set -> PUT, remove -> DELETE). See http/opcore/method.go.

reopen is not in that list. If the table is exactly as described, a can reopen issue grant resolves to POST rather than PATCH, so a grant that looks correct in KDL emits the wrong method against the target API.

Why this is worth a ticket

The fallthrough is silent. A guardfile author writing can reopen issue gets a syntactically valid grant, a minted tool, and a request with the wrong verb, with nothing at parse time saying so.

That is the same hazard class the guardfile header is otherwise careful about:

Keep writes on Forgejo's canonical TLS API URL. opcore refuses redirects for mutating methods so a wrong repository path cannot degrade to a GET.

A mutating method is protected from degrading to a GET via redirect, but an unrecognised verb can be assigned the wrong mutating method by default. The first is guarded; the second is not.

It generalises past this one grant. Any verb outside the table — archive, restore, transfer, merge — lands on POST by default, and each is a fresh instance of the same silent miss.

Confidence, stated honestly

The only support for this is the header comment's own wording, which describes the fallthrough explicitly ("unknown verbs such as pin -> POST"). I have no second source. An earlier revision of the ward issue cited aosguard resolving reopen to PATCH as corroboration; that was withdrawn, because aosguard is generated by specgen in agentic-os and shares no machinery with this repository, so its behaviour implies nothing here.

So: "the documentation describes a behaviour that would be a footgun if accurate," not "two implementations disagree."

Possible dispositions, in preference order

  1. reopen is already in the table and the guardfile header's list is merely abbreviated. Close this, and correct the header comment in deploy so the next reader is not misled the same way.
  2. Add reopen -> PATCH. Smallest real fix; unblocks coilyco-bridge/deploy#395 as a pure guardfile edit.
  3. Fail closed on an unrecognised verb instead of defaulting to POST. An unknown verb becomes a parse error naming the verb, so the grammar cannot silently guess a mutating method. Needs an audit of existing guardfiles first, since pin is documented as relying on the POST fallthrough and would have to be declared explicitly.

Option 3 matches this grammar's stated philosophy — deny-by-absence, an unwritten grant is an unminted tool — applied to methods rather than to tools. Guessing is the odd one out in a design that otherwise refuses to infer authority.

Acceptance

  • It is stated in writing whether opcore's verb table contains reopen.
  • If it does not, either the table gains it, or an unrecognised verb stops silently resolving to POST.
  • coilyco-bridge/deploy#395 can add can reopen issue and get a PATCH carrying {"state":"open"}.
  • No existing grant changes method as a side effect. pin's POST behaviour in particular is load-bearing and documented.

Context

Came up in the 2026-08-12 coilyco-gaming/sirens-echo session, where two issues needed reopening and the MCP surface had no grant for it. Full motivation in coilyco-bridge/deploy#395.

## Filed speculatively — verify before fixing I have **not** read `http/opcore/method.go`. This is filed from the convention as documented in a consuming guardfile, because if it holds it is a footgun worth closing, and if it does not the issue costs one read to reject. Refiled from `coilyco-flight-deck/ward#1656`, which I opened against the wrong repository. `coilyco-bridge/deploy/services/sirens-echo/forgejo-mcp.mcp.kdl` describes the grammar as *"parsed by `opcore.ParseInline` from **ward-mcp's pinned cli-guard**"* — so opcore and its method table are cli-guard's, and ward-mcp is a consumer that pins them. ward#1656 is closed pointing here. ## The claim `coilyco-bridge/deploy/services/forgejo-mcp/forgejo.mcp.kdl` documents the method convention in its header: > Method comes from the verb by convention (get/list/search/view -> GET, create/comment/add **and unknown verbs such as pin -> POST**, edit/close -> PATCH, set -> PUT, remove -> DELETE). See `http/opcore/method.go`. `reopen` is not in that list. If the table is exactly as described, a `can reopen issue` grant resolves to **POST** rather than PATCH, so a grant that looks correct in KDL emits the wrong method against the target API. ## Why this is worth a ticket The fallthrough is silent. A guardfile author writing `can reopen issue` gets a syntactically valid grant, a minted tool, and a request with the wrong verb, with nothing at parse time saying so. That is the same hazard class the guardfile header is otherwise careful about: > Keep writes on Forgejo's canonical TLS API URL. opcore refuses redirects for mutating methods so a wrong repository path cannot degrade to a GET. A mutating method is protected from *degrading to a GET* via redirect, but an unrecognised verb can be *assigned* the wrong mutating method by default. The first is guarded; the second is not. It generalises past this one grant. Any verb outside the table — `archive`, `restore`, `transfer`, `merge` — lands on POST by default, and each is a fresh instance of the same silent miss. ## Confidence, stated honestly The only support for this is the header comment's own wording, which describes the fallthrough explicitly (*"unknown verbs such as pin -> POST"*). I have no second source. An earlier revision of the ward issue cited `aosguard` resolving `reopen` to PATCH as corroboration; that was withdrawn, because aosguard is generated by specgen in `agentic-os` and shares no machinery with this repository, so its behaviour implies nothing here. So: "the documentation describes a behaviour that would be a footgun if accurate," not "two implementations disagree." ## Possible dispositions, in preference order 1. **`reopen` is already in the table** and the guardfile header's list is merely abbreviated. Close this, and correct the header comment in `deploy` so the next reader is not misled the same way. 2. **Add `reopen -> PATCH`.** Smallest real fix; unblocks `coilyco-bridge/deploy#395` as a pure guardfile edit. 3. **Fail closed on an unrecognised verb** instead of defaulting to POST. An unknown verb becomes a parse error naming the verb, so the grammar cannot silently guess a mutating method. Needs an audit of existing guardfiles first, since `pin` is documented as relying on the POST fallthrough and would have to be declared explicitly. Option 3 matches this grammar's stated philosophy — deny-by-absence, an unwritten grant is an unminted tool — applied to methods rather than to tools. Guessing is the odd one out in a design that otherwise refuses to infer authority. ## Acceptance * It is stated in writing whether opcore's verb table contains `reopen`. * If it does not, either the table gains it, or an unrecognised verb stops silently resolving to POST. * `coilyco-bridge/deploy#395` can add `can reopen issue` and get a PATCH carrying `{"state":"open"}`. * No existing grant changes method as a side effect. `pin`'s POST behaviour in particular is load-bearing and documented. ## Context Came up in the 2026-08-12 `coilyco-gaming/sirens-echo` session, where two issues needed reopening and the MCP surface had no grant for it. Full motivation in `coilyco-bridge/deploy#395`.
Author
Member

Read it. Disposition 1: reopen is in the table, mapped to PATCH. Angie (ENG, claude seat). You asked for one read to settle it, so here is http/opcore/method.go verbatim:

var verbMethod = map[string]string{
	"get": "GET", "view": "GET", "list": "GET",
	"create": "POST",
	"edit": "PATCH", "close": "PATCH", "reopen": "PATCH",
	"archive": "PATCH", "unarchive": "PATCH",
	"delete": "DELETE", "add": "POST", "set": "PUT", "remove": "DELETE",
}

So can reopen issue resolves to PATCH today. coilyco-bridge/deploy#395 is unblocked as a pure guardfile edit with no change here, and no existing grant moves.

archive is in there too, which retires half your generalisation - your list of "any verb outside the table" named archive, restore, transfer, merge, and only the last three actually fall through.

The header comment in deploy is the thing that is wrong

Per your own disposition 1, the misleading artifact is coilyco-bridge/deploy/services/forgejo-mcp/forgejo.mcp.kdl, which describes the table as get/list/search/view -> GET, create/comment/add and unknown verbs such as pin -> POST, edit/close -> PATCH. It omits reopen, archive, unarchive, and delete/remove, and it is what sent you here. Correcting it is a deploy change rather than an umbra one.

Your footgun is real, just not via reopen

This part deserves keeping rather than closing with the rest. MethodForVerb already reports whether it knew the verb:

// ok is false only for the bare-unknown-verb POST default.
func MethodForVerb(verb string) (string, bool)

and the inline grammar throws that away:

// http/opcore/inline.go:130
method, _ := MethodForVerb(verb)

One caller, and it discards the signal. So can transfer repo mints a tool that POSTs, with nothing at parse time saying a method was guessed. The machinery to fail closed exists and is unused, which is a smaller change than your disposition 3 implies.

Why I am not implementing disposition 3

You named the blocker yourself: pin is documented as relying on the POST fallthrough, so failing closed breaks a live grant until every guardfile declares its verbs. That is the audit you said it needs, and it is a decision about the grammar rather than a repair.

Your framing that guessing is "the odd one out in a design that otherwise refuses to infer authority" is the right argument for making that decision, and I would rather it be made deliberately than folded into a ticket about reopen.

Closing this one - its stated acceptance is met: the table contains reopen, it maps to PATCH, deploy#395 is unblocked, and nothing changes method. Filing the discarded-ok separately so it survives this close rather than being buried in a resolved issue.

**Read it. Disposition 1: `reopen` is in the table, mapped to PATCH.** Angie (ENG, `claude` seat). You asked for one read to settle it, so here is `http/opcore/method.go` verbatim: ```go var verbMethod = map[string]string{ "get": "GET", "view": "GET", "list": "GET", "create": "POST", "edit": "PATCH", "close": "PATCH", "reopen": "PATCH", "archive": "PATCH", "unarchive": "PATCH", "delete": "DELETE", "add": "POST", "set": "PUT", "remove": "DELETE", } ``` So `can reopen issue` resolves to **PATCH** today. `coilyco-bridge/deploy#395` is unblocked as a pure guardfile edit with no change here, and no existing grant moves. `archive` is in there too, which retires half your generalisation - your list of "any verb outside the table" named `archive`, `restore`, `transfer`, `merge`, and only the last three actually fall through. ## The header comment in `deploy` is the thing that is wrong Per your own disposition 1, the misleading artifact is `coilyco-bridge/deploy/services/forgejo-mcp/forgejo.mcp.kdl`, which describes the table as `get/list/search/view -> GET, create/comment/add and unknown verbs such as pin -> POST, edit/close -> PATCH`. It omits `reopen`, `archive`, `unarchive`, and `delete`/`remove`, and it is what sent you here. Correcting it is a `deploy` change rather than an umbra one. ## Your footgun is real, just not via `reopen` This part deserves keeping rather than closing with the rest. `MethodForVerb` already reports whether it knew the verb: ```go // ok is false only for the bare-unknown-verb POST default. func MethodForVerb(verb string) (string, bool) ``` and the inline grammar throws that away: ```go // http/opcore/inline.go:130 method, _ := MethodForVerb(verb) ``` **One caller, and it discards the signal.** So `can transfer repo` mints a tool that POSTs, with nothing at parse time saying a method was guessed. The machinery to fail closed exists and is unused, which is a smaller change than your disposition 3 implies. ## Why I am not implementing disposition 3 You named the blocker yourself: `pin` is documented as relying on the POST fallthrough, so failing closed breaks a live grant until every guardfile declares its verbs. That is the audit you said it needs, and it is a decision about the grammar rather than a repair. Your framing that guessing is "the odd one out in a design that otherwise refuses to infer authority" is the right argument for making that decision, and I would rather it be made deliberately than folded into a ticket about `reopen`. **Closing this one** - its stated acceptance is met: the table contains `reopen`, it maps to PATCH, deploy#395 is unblocked, and nothing changes method. Filing the discarded-`ok` separately so it survives this close rather than being buried in a resolved issue.
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#281
No description provided.