feat(opcore) [PR3/3 of #196]: inline-operation source (ParseInline) for the ward-mcp .mcp.kdl grammar #199

Closed
opened 2026-07-07 08:28:18 +00:00 by coilysiren · 2 comments
Owner

Scope (PR3 of the cli-guard#196 split - the inline-operation source)

Add a second Descriptor source that states the descriptor from inline KDL instead of resolving it against an OpenAPI spec, mirroring how execverb.Parse (cli/execverb/guardfile.go:155) authors exec commands inline with no spec. Blocked on PR1. Co-designs with ward-mcp#6 (the grammar).

Lives in http/opcore (urfave/cli-free) - not in specverb, or it would re-couple MCP to the CLI projection.

Add to http/opcore

func ParseInline(src []byte) ([]Descriptor, RuntimeConfig, error)

Parses the ward-mcp#6 grammar, frozen as can <verb> <resource> { path "..."; query "..."; body "..."; set k=v }:

  • Uses github.com/calico32/kdl-go, same node-walking shape as guardfile.parseGrant / execverb.parseGrant.
  • Method inferred from the verb via opcore.MethodForVerb (extracted from verbConventions in PR1): create→POST, get/view/list/search→GET, edit→PATCH/PUT, delete→DELETE, the state toggles and membership verbs already enumerated in verbConventions.
  • Path params inferred from the {template} via the existing pathParamsInOrder(path) / pathParamRe (http/specverb/request.go:303 region) - no operationId, no op resolution.
  • query "a" "b"Field{Location: query}; body "x" "y" → body Fields; set state="closed"FixedBody (the KDL-native RawValue() path, http/guardfile/guardfile.go:688).
  • Also parses the wrap header + base-url + auth <scheme> { value env <VAR> } + restrict <param> matches <glob> into the RuntimeConfig that builds an opcore.Runtime (the auth env-value provider, base-url, restrictions).
  • Reuses checkFlagCollisions (http/specverb/specverb.go:433) so an inline descriptor fails closed on a reserved-flag clash exactly like a resolved one.

Output is a slice of the same opcore.Descriptor the OpenAPI source feeds, so every downstream projection is source-blind. Upstream-agnostic: the KDL carries generic HTTP-op structure, no vendor strings, no operationIds.

Done

  • ParseInline in opcore, with tests over the frozen ward-mcp#6 grammar: method-from-verb, path-params-from-template, set→FixedBody, auth/restrict/base-url → RuntimeConfig, and reserved-flag-collision fail-closed.
  • The same Descriptor type feeds both sources (a shared golden: an inline op and an equivalent resolved op produce equal descriptors).
  • No urfave/cli import in opcore.
  • ward vet / ward test green.

cli-guard#196 (design), PR1 (the extraction + MethodForVerb), ward-mcp#6 (the grammar this parses), ward-mcp#7 (the runtime that calls ParseInline + Execute).

## Scope (PR3 of the cli-guard#196 split - the inline-operation source) Add a second `Descriptor` source that **states** the descriptor from inline KDL instead of resolving it against an OpenAPI spec, mirroring how `execverb.Parse` (`cli/execverb/guardfile.go:155`) authors exec commands inline with no spec. **Blocked on PR1**. Co-designs with ward-mcp#6 (the grammar). Lives in `http/opcore` (urfave/cli-free) - **not** in specverb, or it would re-couple MCP to the CLI projection. ## Add to `http/opcore` ```go func ParseInline(src []byte) ([]Descriptor, RuntimeConfig, error) ``` Parses the ward-mcp#6 grammar, frozen as `can <verb> <resource> { path "..."; query "..."; body "..."; set k=v }`: - Uses `github.com/calico32/kdl-go`, same node-walking shape as `guardfile.parseGrant` / `execverb.parseGrant`. - **Method inferred from the verb** via `opcore.MethodForVerb` (extracted from `verbConventions` in PR1): create→POST, get/view/list/search→GET, edit→PATCH/PUT, delete→DELETE, the state toggles and membership verbs already enumerated in `verbConventions`. - **Path params inferred from the `{template}`** via the existing `pathParamsInOrder(path)` / `pathParamRe` (`http/specverb/request.go:303` region) - no operationId, no `op` resolution. - `query "a" "b"` → `Field{Location: query}`; `body "x" "y"` → body `Field`s; `set state="closed"` → `FixedBody` (the KDL-native `RawValue()` path, `http/guardfile/guardfile.go:688`). - Also parses the wrap header + `base-url` + `auth <scheme> { value env <VAR> }` + `restrict <param> matches <glob>` into the `RuntimeConfig` that builds an `opcore.Runtime` (the auth env-value provider, base-url, restrictions). - Reuses `checkFlagCollisions` (`http/specverb/specverb.go:433`) so an inline descriptor fails closed on a reserved-flag clash exactly like a resolved one. Output is a slice of the **same** `opcore.Descriptor` the OpenAPI source feeds, so every downstream projection is source-blind. Upstream-agnostic: the KDL carries generic HTTP-op structure, no vendor strings, no operationIds. ## Done - `ParseInline` in opcore, with tests over the frozen ward-mcp#6 grammar: method-from-verb, path-params-from-template, `set`→FixedBody, `auth`/`restrict`/`base-url` → RuntimeConfig, and reserved-flag-collision fail-closed. - The same `Descriptor` type feeds both sources (a shared golden: an inline op and an equivalent resolved op produce equal descriptors). - No urfave/cli import in opcore. - `ward vet` / `ward test` green. ## Related cli-guard#196 (design), PR1 (the extraction + `MethodForVerb`), ward-mcp#6 (the grammar this parses), ward-mcp#7 (the runtime that calls ParseInline + Execute).
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-cli-guard-199 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-07T09:08:03Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-flight-deck/cli-guard#199 · branch issue-199 · driver claude · workflow direct-main
  • Run: engineer-claude-cli-guard-199 · ward v0.419.0 · dispatched 2026-07-07T09:08:03Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

## Scope (PR3 of the cli-guard#196 split - the inline-operation source)

Add a second `Descriptor` source that **states** the descriptor from inline KDL instead of resolving it against an OpenAPI spec, mirroring how `execverb.Parse` (`cli/execverb/guardfile.go:155`) authors exec commands inline with no spec. **Blocked on PR1**. Co-designs with ward-mcp#6 (the grammar).

Lives in `http/opcore` (urfave/cli-free) - **not** in specverb, or it would re-couple MCP to the CLI projection.

## Add to `http/opcore`

` ` `go
func ParseInline(src []byte) ([]Descriptor, RuntimeConfig, error)
` ` `

Parses the ward-mcp#6 grammar, frozen as `can <verb> <resource> { path "..."; query "..."; body "..."; set k=v }`:

- Uses `github.com/calico32/kdl-go`, same node-walking shape as `guardfile.parseGrant` / `execverb.parseGrant`.
- **Method inferred from the verb** via `opcore.MethodForVerb` (extracted from `verbConventions` in PR1): create→POST, get/view/list/search→GET, edit→PATCH/PUT, delete→DELETE, the state toggles and membership verbs already enumerated in `verbConventions`.
- **Path params inferred from the `{template}`** via the existing `pathParamsInOrder(path)` / `pathParamRe` (`http/specverb/request.go:303` region) - no operationId, no `op` resolution.
- `query "a" "b"` → `Field{Location: query}`; `body "x" "y"` → body `Field`s; `set state="closed"` → `FixedBody` (the KDL-native `RawValue()` path, `http/guardfile/guardfile.go:688`).
- Also parses the wrap header + `base-url` + `auth <scheme> { value env <VAR> }` + `restrict <param> matches <glob>` into the `RuntimeConfig` that builds an `opcore.Runtime` (the auth env-value provider, base-url, restrictions).
- Reuses `checkFlagCollisions` (`http/specverb/specverb.go:433`) so an inline descriptor fails closed on a reserved-flag clash exactly like a resolved one.

Output is a slice of the **same** `opcore.Descriptor` the OpenAPI source feeds, so every downstream projection is source-blind. Upstream-agnostic: the KDL ca

… (truncated to 2000 chars; full body is on this issue)

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-cli-guard-199` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-07T09:08:03Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-flight-deck/cli-guard#199` · branch `issue-199` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-cli-guard-199` · ward `v0.419.0` · dispatched `2026-07-07T09:08:03Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). **Issue body as seeded:** ``` ## Scope (PR3 of the cli-guard#196 split - the inline-operation source) Add a second `Descriptor` source that **states** the descriptor from inline KDL instead of resolving it against an OpenAPI spec, mirroring how `execverb.Parse` (`cli/execverb/guardfile.go:155`) authors exec commands inline with no spec. **Blocked on PR1**. Co-designs with ward-mcp#6 (the grammar). Lives in `http/opcore` (urfave/cli-free) - **not** in specverb, or it would re-couple MCP to the CLI projection. ## Add to `http/opcore` ` ` `go func ParseInline(src []byte) ([]Descriptor, RuntimeConfig, error) ` ` ` Parses the ward-mcp#6 grammar, frozen as `can <verb> <resource> { path "..."; query "..."; body "..."; set k=v }`: - Uses `github.com/calico32/kdl-go`, same node-walking shape as `guardfile.parseGrant` / `execverb.parseGrant`. - **Method inferred from the verb** via `opcore.MethodForVerb` (extracted from `verbConventions` in PR1): create→POST, get/view/list/search→GET, edit→PATCH/PUT, delete→DELETE, the state toggles and membership verbs already enumerated in `verbConventions`. - **Path params inferred from the `{template}`** via the existing `pathParamsInOrder(path)` / `pathParamRe` (`http/specverb/request.go:303` region) - no operationId, no `op` resolution. - `query "a" "b"` → `Field{Location: query}`; `body "x" "y"` → body `Field`s; `set state="closed"` → `FixedBody` (the KDL-native `RawValue()` path, `http/guardfile/guardfile.go:688`). - Also parses the wrap header + `base-url` + `auth <scheme> { value env <VAR> }` + `restrict <param> matches <glob>` into the `RuntimeConfig` that builds an `opcore.Runtime` (the auth env-value provider, base-url, restrictions). - Reuses `checkFlagCollisions` (`http/specverb/specverb.go:433`) so an inline descriptor fails closed on a reserved-flag clash exactly like a resolved one. Output is a slice of the **same** `opcore.Descriptor` the OpenAPI source feeds, so every downstream projection is source-blind. Upstream-agnostic: the KDL ca ``` … (truncated to 2000 chars; full body is on this issue) Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - opcore.ParseInline lands as the second Descriptor source (inline ward-mcp KDL), byte-for-byte equal to a resolved op.

This one went smoothly, largely because PR1 had already carved out the right seams: MethodForVerb was sitting in opcore waiting, and pathParamRe was already there in the binder, so the inline parser was mostly assembling parts that existed. The satisfying moment was the shared golden going green on the first run - an inline get repo and a spec-resolved get repo compared with reflect.DeepEqual and matched exactly, which is the real proof that both sources feed one source-blind type.

A couple of things fought back, none deep. The layering meant checkFlagCollisions, reservedFlagNames, and destructiveVerbs had to move down from specverb into opcore (specverb can't be imported upward), so I aliased them back so specverb's call sites and tests stayed put. The base-url refactor bit me once: my first ParseBaseURL clobbered the string field with an empty when a block node followed, which silently killed guardfile's existing two-forms fail-closed test - a good reminder that the accumulate-then-validate shape was load-bearing. And then a long tail of pre-commit comment-cap and the 4000-char FEATURES.md cap, which cost more edits than the actual code.

Confidence is high: engine stays urfave/cli-free, the fail-closed surface is well-tested, and the golden pins the contract. Rough edges worth a follow-up: inline query/body fields are all typed string (no schema to draw from), so if ward-mcp ever needs typed inline inputs that's a grammar extension; and only the can modal is accepted inline (no cannot/never), which is right for MCP but worth stating if the grammar grows. Reused guardfile's auth/restrict parsers directly, so auth semantics can't drift between the two sources.

WARD-OUTCOME: done - opcore.ParseInline lands as the second Descriptor source (inline ward-mcp KDL), byte-for-byte equal to a resolved op. This one went smoothly, largely because PR1 had already carved out the right seams: MethodForVerb was sitting in opcore waiting, and pathParamRe was already there in the binder, so the inline parser was mostly assembling parts that existed. The satisfying moment was the shared golden going green on the first run - an inline `get repo` and a spec-resolved `get repo` compared with reflect.DeepEqual and matched exactly, which is the real proof that both sources feed one source-blind type. A couple of things fought back, none deep. The layering meant `checkFlagCollisions`, `reservedFlagNames`, and `destructiveVerbs` had to move down from specverb into opcore (specverb can't be imported upward), so I aliased them back so specverb's call sites and tests stayed put. The base-url refactor bit me once: my first ParseBaseURL clobbered the string field with an empty when a block node followed, which silently killed guardfile's existing two-forms fail-closed test - a good reminder that the accumulate-then-validate shape was load-bearing. And then a long tail of pre-commit comment-cap and the 4000-char FEATURES.md cap, which cost more edits than the actual code. Confidence is high: engine stays urfave/cli-free, the fail-closed surface is well-tested, and the golden pins the contract. Rough edges worth a follow-up: inline query/body fields are all typed `string` (no schema to draw from), so if ward-mcp ever needs typed inline inputs that's a grammar extension; and only the `can` modal is accepted inline (no cannot/never), which is right for MCP but worth stating if the grammar grows. Reused guardfile's auth/restrict parsers directly, so auth semantics can't drift between the two sources.
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#199
No description provided.