Spec-driven ward: replace per-verb Go wrappers with annotated upstream specs #51

Closed
opened 2026-06-04 11:13:46 +00:00 by coilysiren · 2 comments
Owner

Problem

ward currently carries per-verb wrapper code in Go for each wrapped binary (make, go, gh, aws, kubectl, ...). Adding or adjusting a wrapped surface means writing more Go. The policy that matters (which subcommands allowed, which flags rejected, which argv shapes refused) is a small fraction of the code; the rest is argv parsing and shape boilerplate that re-implements what the upstream tool already publishes.

Proposal

Move ward toward a spec-driven model: a thin engine that consumes annotated upstream command specs and enforces policy declaratively, instead of hand-rolled Go wrappers per binary.

Rough shape:

  1. Spec source per binary. Declare how to discover the upstream tool's command tree:
    • Cobra / urfave-cli: shell completion dump (__complete, --generate-shell-completion) or built-in JSON introspection.
    • Makefile: make -pn parse for target list.
    • AWS / kubectl: --generate-cli-skeleton / api-resources -o json.
    • Hand-written JSON fallback for tools that ship nothing machine-readable.
  2. Annotation layer. A YAML/JSON sidecar on top of the discovered spec marking (a) which subcommands are allowed, (b) which flags are allowed/denied, (c) argv-pattern rules (regex / shell-metachar rejection). This is the security part, the part that has to be written by hand. Everything else is derived.
  3. Engine in cli-guard. Walks the merged (spec + annotation) at runtime, validates argv, then dispatches. No per-binary Go file.
  4. .ward/ward.yaml stays the per-repo binding (which verbs map to which Makefile targets), but the verb implementations stop being Go.

Prior art worth pulling from

  • Fig completion-spec — thousands of CLIs already spec'd as MIT-licensed TypeScript/JSON. Covers shape (subcommands, flag types) but not semantics — useful as the spec source, not the policy source.
  • OPA/Rego over argv — overkill for v0, but worth knowing the pattern exists if the annotation DSL grows.
  • urfave/cli introspection JSON — relevant because cli-guard's sibling repos already use urfave/cli, so dogfooding is cheap.

The honest tradeoff

The win is deleting per-verb Go and the argv-parsing reimplementation. The policy annotations still have to be written by hand — Fig-style specs describe shape, not danger. So this is a code-shape change, not a code-elimination change. Realistic reduction is the per-verb wrapper layer; the policy DSL stays roughly the same size as today's allowlist YAML.

Out of scope for this issue

  • Picking the spec format (Fig vs hand-rolled JSON vs something else).
  • Whether the engine lives in ward or moves up into cli-guard.
  • Migration path for existing downstream .ward/ward.yaml consumers.

Those are follow-ups once the direction is settled.

Filed by

Filed by Claude during a ward design conversation with Kai.

## Problem `ward` currently carries per-verb wrapper code in Go for each wrapped binary (`make`, `go`, `gh`, `aws`, `kubectl`, ...). Adding or adjusting a wrapped surface means writing more Go. The policy that matters (which subcommands allowed, which flags rejected, which argv shapes refused) is a small fraction of the code; the rest is argv parsing and shape boilerplate that re-implements what the upstream tool already publishes. ## Proposal Move `ward` toward a spec-driven model: a thin engine that consumes annotated upstream command specs and enforces policy declaratively, instead of hand-rolled Go wrappers per binary. Rough shape: 1. **Spec source per binary.** Declare how to discover the upstream tool's command tree: - Cobra / urfave-cli: shell completion dump (`__complete`, `--generate-shell-completion`) or built-in JSON introspection. - Makefile: `make -pn` parse for target list. - AWS / kubectl: `--generate-cli-skeleton` / `api-resources -o json`. - Hand-written JSON fallback for tools that ship nothing machine-readable. 2. **Annotation layer.** A YAML/JSON sidecar on top of the discovered spec marking (a) which subcommands are allowed, (b) which flags are allowed/denied, (c) argv-pattern rules (regex / shell-metachar rejection). This is the *security* part, the part that has to be written by hand. Everything else is derived. 3. **Engine in cli-guard.** Walks the merged (spec + annotation) at runtime, validates argv, then dispatches. No per-binary Go file. 4. **`.ward/ward.yaml` stays the per-repo binding** (which verbs map to which Makefile targets), but the verb implementations stop being Go. ## Prior art worth pulling from - **[Fig completion-spec](https://fig.io/)** — thousands of CLIs already spec'd as MIT-licensed TypeScript/JSON. Covers shape (subcommands, flag types) but not semantics — useful as the spec source, not the policy source. - **OPA/Rego over argv** — overkill for v0, but worth knowing the pattern exists if the annotation DSL grows. - **urfave/cli introspection JSON** — relevant because cli-guard's sibling repos already use urfave/cli, so dogfooding is cheap. ## The honest tradeoff The win is deleting per-verb Go and the argv-parsing reimplementation. The *policy* annotations still have to be written by hand — Fig-style specs describe shape, not danger. So this is a code-shape change, not a code-elimination change. Realistic reduction is the per-verb wrapper layer; the policy DSL stays roughly the same size as today's allowlist YAML. ## Out of scope for this issue - Picking the spec format (Fig vs hand-rolled JSON vs something else). - Whether the engine lives in `ward` or moves up into `cli-guard`. - Migration path for existing downstream `.ward/ward.yaml` consumers. Those are follow-ups once the direction is settled. ## Filed by Filed by Claude during a `ward` design conversation with Kai.
Author
Owner

Folded into a shared cli-guard engine + policy DSL: cli-guard#75 (specverb + KDL Guardfile). Same shape as this issue (thin engine over an annotated upstream spec) but unified across ward/coily/kap, with the annotation layer authored in KDL (non-executable, so no eval attack surface) compiling down to a standard OpenAPI Overlay.

Folded into a shared cli-guard engine + policy DSL: [cli-guard#75](https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/75) (specverb + KDL Guardfile). Same shape as this issue (thin engine over an annotated upstream spec) but unified across ward/coily/kap, with the annotation layer authored in KDL (non-executable, so no eval attack surface) compiling down to a standard OpenAPI Overlay.
Author
Owner

Backlog burndown 2026-06-17: closing low-priority (P3/P4) to bring the open count to a manageable level. Nothing lost — reopen if this resurfaces. Batch tag: burndown-2026-06.

Backlog burndown 2026-06-17: closing low-priority (P3/P4) to bring the open count to a manageable level. Nothing lost — reopen if this resurfaces. Batch tag: `burndown-2026-06`.
coilysiren 2026-06-17 08:21:59 +00:00
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/ward#51
No description provided.