specverb: auto-resolve op by path+method convention, op becomes override #147

Closed
opened 2026-06-17 16:01:03 +00:00 by coilysiren · 2 comments
Owner

Problem

After ward#146 / commit 3dd1d4d (delete the engine expansion table), every can grant must hand-bind op "<operationId>". That made the engine durable across specs but pushed vendor-string boilerplate into every guardfile (createCurrentUserRepo, issueGetMilestonesList, ...). The Forgejo guardfile alone carried ~39 explicit ops.

Change

Resolve op automatically by generic REST convention, with op retained as an override seam. specverb.resolveOp (new, http/specverb/resolve.go) maps the canonical CRUD verbs (get/view/list/create/edit/delete) to an operation by HTTP method + the path's resource segment, against the embedded spec. Spec-agnostic: pure path+method, no vendor strings in engine code; the OpenAPI-3 reader lowers into the same internal model, so one resolver drives Swagger 2 / OpenAPI 3.x.

  • op becomes optional on can; explicit op always wins (irregular endpoints, fixed-body toggles, ambiguous matches).
  • Fail-closed: zero candidates, multiple candidates, or a non-CRUD verb is an error that names the candidates.
  • Wired into resolveDescriptor (specverb.go) and grantedPathMethods (prune.go) via the one resolveOp seam.

Decision

Pure runtime resolution, nothing recorded (no lock-table). Determinism comes from the vendored+locked+skew-checked spec; auditability from describe + fail-closed build errors.

Verification

Resolver reproduces the exact explicit op for every convention-resolvable grant across all three real ward specs (Forgejo/Tailscale/Trello) with zero mismatches. New resolve_test.go covers convention/override/fail-closed/ambiguous/PUT-fallback. Suite + vet + lint + godoc + doc-caps green.

Downstream: ward drops op on the resolvable grants in its four guardfiles.

## Problem After ward#146 / commit `3dd1d4d` (delete the engine expansion table), every `can` grant must hand-bind `op "<operationId>"`. That made the engine durable across specs but pushed vendor-string boilerplate into every guardfile (`createCurrentUserRepo`, `issueGetMilestonesList`, ...). The Forgejo guardfile alone carried ~39 explicit ops. ## Change Resolve `op` automatically by generic REST convention, with `op` retained as an override seam. `specverb.resolveOp` (new, `http/specverb/resolve.go`) maps the canonical CRUD verbs (`get`/`view`/`list`/`create`/`edit`/`delete`) to an operation by HTTP method + the path's resource segment, against the embedded spec. Spec-agnostic: pure path+method, no vendor strings in engine code; the OpenAPI-3 reader lowers into the same internal model, so one resolver drives Swagger 2 / OpenAPI 3.x. - `op` becomes optional on `can`; explicit `op` always wins (irregular endpoints, fixed-body toggles, ambiguous matches). - Fail-closed: zero candidates, multiple candidates, or a non-CRUD verb is an error that names the candidates. - Wired into `resolveDescriptor` (specverb.go) and `grantedPathMethods` (prune.go) via the one `resolveOp` seam. ## Decision Pure runtime resolution, nothing recorded (no lock-table). Determinism comes from the vendored+locked+skew-checked spec; auditability from `describe` + fail-closed build errors. ## Verification Resolver reproduces the exact explicit op for every convention-resolvable grant across all three real ward specs (Forgejo/Tailscale/Trello) with zero mismatches. New `resolve_test.go` covers convention/override/fail-closed/ambiguous/PUT-fallback. Suite + vet + lint + godoc + doc-caps green. Downstream: ward drops `op` on the resolvable grants in its four guardfiles.
Member

🔒 Reserved by ward agent claude — container ward-cli-guard-issue-147-claude-1ebfdd62 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-23T08:29:30Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-cli-guard-issue-147-claude-1ebfdd62` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-23T08:29:30Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

Resolved and on canonical main. The convention resolver landed in c7d4c9b (feat(specverb): auto-resolve op by path+method convention, op becomes override) and was extended by f8b71b2 (full generic convention set) and ef765d0 (operationId-word fallback) — all ancestors of main/origin/main.

Delivered exactly as scoped:

  • specverb.resolveOp (http/specverb/resolve.go) — spec-agnostic path+method → operation; one resolver over Swagger 2.0 / OpenAPI 3.x.
  • op now optional on can; explicit op always wins (override seam).
  • Fail-closed on zero / multiple candidates / non-CRUD verb, naming the candidates.
  • Wired through the single resolveOp seam from resolveDescriptor (specverb.go) and grantedPathMethods (prune.go); guardfile parser no longer requires op.
  • resolve_test.go covers convention / override / fail-closed / ambiguous / PUT-fallback. docs/specverb-resolution.md added.

Those commits referenced the issue URL but never used a closes keyword, so it stayed open. Closing now — make test shows the specverb suite green; the only failures are the privilege-dependent cli/sandbox seccomp/ptrace/jail tests, which can't run inside this nested container and are unrelated to this change.

closes #147

— Claude (she/her), via ward agent

Resolved and on canonical `main`. The convention resolver landed in `c7d4c9b` (`feat(specverb): auto-resolve op by path+method convention, op becomes override`) and was extended by `f8b71b2` (full generic convention set) and `ef765d0` (operationId-word fallback) — all ancestors of `main`/`origin/main`. Delivered exactly as scoped: - `specverb.resolveOp` (`http/specverb/resolve.go`) — spec-agnostic path+method → operation; one resolver over Swagger 2.0 / OpenAPI 3.x. - `op` now optional on `can`; explicit `op` always wins (override seam). - Fail-closed on zero / multiple candidates / non-CRUD verb, naming the candidates. - Wired through the single `resolveOp` seam from `resolveDescriptor` (specverb.go) and `grantedPathMethods` (prune.go); guardfile parser no longer requires `op`. - `resolve_test.go` covers convention / override / fail-closed / ambiguous / PUT-fallback. `docs/specverb-resolution.md` added. Those commits referenced the issue URL but never used a `closes` keyword, so it stayed open. Closing now — `make test` shows the specverb suite green; the only failures are the privilege-dependent `cli/sandbox` seccomp/ptrace/jail tests, which can't run inside this nested container and are unrelated to this change. closes #147 — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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/cli-guard#147
No description provided.