specverb + KDL Guardfile: spec-driven verb engine with a non-executable policy DSL #75

Closed
opened 2026-06-05 02:47:30 +00:00 by coilysiren · 4 comments
Owner

Summary

Replace the per-verb, hand-rolled CLI wrappers in cli-guard's consumers with a spec-driven engine plus a KDL policy DSL. One generic Go engine (specverb) builds the guarded cli.Command tree at runtime from an embedded, pinned API spec plus a compiled overlay. The overlay is authored as a human-readable, non-executable KDL Guardfile that compiles down to a standard OpenAPI Overlay.

This unifies two existing consumer issues into one piece of cli-guard infra:

  • coilyco-bridge/coily#186 - "Backfill the Forgejo OpenAPI spec into coily (generate verbs, stop hand-rolling)" - the REST surface.
  • coilyco-flight-deck/ward#51 - "Spec-driven ward: replace per-verb Go wrappers with annotated upstream specs" - the CLI-argv surface.

Both are the same shape: a thin engine over an annotated upstream spec, behind cli-guard's policy + audit boundary. Build it once in cli-guard, consumed by ward, coily, and kap.

The layer stack

  • L0 - upstream spec. The vendor's truth about what the API can do (e.g. Forgejo's swagger.v1.json).
  • L1 - overlay + x-coily-* extensions. The compiled, deterministic policy IR. Standard OAI Overlay (JSON/YAML), targeting the spec by JSONPath. This is the interchange format other tooling (Speakeasy, oas-patch, libopenapi) understands, and it is what the runtime consumes.
  • L2 - KDL Guardfile. The human authoring layer. Pure data, schema-validated. Compiles down to L1.

KDL replaces the authoring layer only. The overlay stays as the compiled artifact so the runtime targets a stable, pinned, hash-verified input.

The runtime engine (specverb, lives in cli-guard)

The generated Go we have today is ~90% static boilerplate (doRequestAndStream, readBody, fetchSSM) plus a tiny per-op descriptor ({method, path, pathParams, queryParams, hasBody, authNeeded, verbName}). A runtime engine builds []opDescriptor from the parsed spec + overlay and binds them to one generic action. The thousands of lines of *_generated.go (and coily's Python openapi-to-coily.py) collapse into one tested engine.

Maximally-generic form: embed the spec + overlay via go:embed, build the cli.Command tree at runtime (lazy/cached), mount under cli-guard's verb.Wrap + audit.Writer.

Deny-by-default allowlist: an operation is mounted if and only if it carries an x-coily-verb annotation. The overlay is then purely additive (all update actions, no fragile remove sweeps), and the set of annotated ops is the allowlist.

x-coily-* vocabulary (what the engine reads)

  • x-coily-verb - REQUIRED to mount. Value "<group> <leaf>". Its presence is the allowlist.
  • x-coily-args - ordered positional args drawn from path params.
  • x-coily-body-flags - promote request-body scalar fields to typed flags (required schema fields -> required flags). Omit -> single raw --body (literal / @file / -).
  • x-coily-fmt - named respfmt hook for curated output. Absent -> pretty JSON.
  • x-coily-resolve - named pre-call hook for non-declarable cases (e.g. label name->id, the coily#159 behavior).
  • x-coily-allow-redirect - defaults false on mutating methods. Set true only to opt out of the 301 refusal (the coily#178 / #160 fix, now structural).

The KDL Guardfile (L2 authoring)

Chosen over a literal-Ruby internal DSL. A Ruby Guardfile would be eval'd - arbitrary code execution at build time, which is attack surface and is backwards for a tool (ward) whose job is to keep hostile code from running. KDL is pure data: parsing, not evaluation, so there is no code to smuggle in. It also ships a schema language (author-time validation) and a CSS-selector query language, and it is in-house (Kat Marchan's language).

wrap "coily ops forgejo" {
    spec "forgejo.swagger.v1.json"
    base-url "https://forgejo.coilysiren.me/api/v1"
    auth "header-token" header="Authorization" prefix="token " ssm="/forgejo/api-token"

    can "read"
    can "create" "edit" on="issues labels repos"   // mounts `repo create`
    cannot "delete" except="label:created-by-me"
    never "force-push"
}

Nodes are the modals (can/cannot/never), arguments are verb-classes, properties carry scope and exceptions.

Security invariants (non-negotiable)

  • Compiler, never interpreter. KDL/LLM resolve to concrete DENY/APPROVE at build time. The compiled, pinned overlay is what enforces at runtime. No model is ever in the request path.
  • Fail closed. Anything the compiler cannot resolve to a concrete op or argv pattern with high confidence becomes DENY and surfaces for human annotation.
  • Embed + pin by hash. Spec + overlay are embedded and pinned, never live-fetched. A live spec would let upstream silently change the attack surface.
  • Guardfile is maintainer-owned (CODEOWNERS). For ward (public contrib), the reviewed artifact is the committed overlay diff. Contributors propose policy in English in the PR body. CI never evals an untrusted Guardfile.

Two enforcement axes (the DSL authors both)

  • Operation-level (cannot delete) -> compiles to L1 overlay (mount/deny ops). The x-coily axis.
  • Argument-value-level (secret get ssn => DENY, secret get login => APPROVE) -> compiles to cli-guard's argv policy (policy / scope / egress). Value-level rules must lower to concrete identifiers/patterns, fail-closed on anything unmatched.

The verb-class -> operation-set expansion (delete catching TerminateInstances, RevokeGrant, ...) is the one fuzzy step. It runs in the compiler with an LLM-assisted-or-curated, human-reviewed, committed expansion table - never at runtime.

Findings from today's spec dig (Forgejo)

  • Forgejo's spec is Swagger 2.0, not OpenAPI 3.x. Body lives in parameters[in:body] (requestBody is null), security is global. The engine needs a 2.0 reader or a 2.0->3.x upgrade pass (kin-openapi openapi2conv).
  • Auth is Authorization: token <key>, not Bearer. securityDefinitions.AuthorizationHeaderToken requires the token prefix. None of the existing generator auth modes cover this - the engine needs a declarable header-token scheme.
  • repo create is genuinely missing today and is the first proving slice: POST /user/repos, operationId: createCurrentUserRepo, body CreateRepoOption (required: name; scalars: private, description, auto_init, default_branch, ...). Org variant: POST /orgs/{org}/repos (createOrgRepo).

First proving slice

Drive coily ops forgejo repo create end-to-end through the chain: KDL Guardfile -> compile -> forgejo.overlay.yaml -> specverb builds the verb -> POST against the live Forgejo API with token auth and body-flag assembly. Validates Swagger-2.0 read, the new auth scheme, deny-by-default mounting, and the 301 default in one shot.

Open decisions

  1. Overlay apply timing. (A) runtime apply (embed raw spec + overlay, apply in Go on first use, purest "Go handles it directly") vs (B) build-time apply, runtime mount (embed a pre-pruned spec, smaller runtime parse). Lean A with a lazy/cached build, fall back to B if Forgejo's large spec makes startup parse hurt. Benchmark before deciding.
  2. KDL parser. calico32/kdl-go (v1+v2, passes upstream suite) keeps the compiler all-Go, vs Kat's reference Rust kdl-rs. Build-time only either way - never ships in the binary.
  3. Engine home. specverb in cli-guard, consumed by ward/coily/kap. Confirm the KDL Guardfile vocabulary also ships from cli-guard (shared), with each CLI keeping its own Guardfile(s) under cmd/.

Prior art

  • OpenAPI Overlay Specification v1.0.0 / latest (OAI). Speakeasy openapi-overlay (Go), pb33f libopenapi, oas-patch (Python).
  • KDL 2.0.0 (kdl.dev) - schema language, KQL query language.
  • kin-openapi openapi2conv for Swagger 2.0 -> OpenAPI 3.x.

Filed by Claude during a design conversation with Kai. Companion to coily#186 and ward#51.

## Summary Replace the per-verb, hand-rolled CLI wrappers in cli-guard's consumers with a **spec-driven engine** plus a **KDL policy DSL**. One generic Go engine (`specverb`) builds the guarded `cli.Command` tree at runtime from an embedded, pinned API spec plus a compiled overlay. The overlay is authored as a human-readable, non-executable **KDL Guardfile** that compiles down to a standard OpenAPI Overlay. This unifies two existing consumer issues into one piece of cli-guard infra: - [coilyco-bridge/coily#186](https://forgejo.coilysiren.me/coilyco-bridge/coily/issues/186) - "Backfill the Forgejo OpenAPI spec into coily (generate verbs, stop hand-rolling)" - the REST surface. - [coilyco-flight-deck/ward#51](https://forgejo.coilysiren.me/coilyco-flight-deck/ward/issues/51) - "Spec-driven ward: replace per-verb Go wrappers with annotated upstream specs" - the CLI-argv surface. Both are the same shape: a thin engine over an annotated upstream spec, behind cli-guard's policy + audit boundary. Build it once in cli-guard, consumed by ward, coily, and kap. ## The layer stack - **L0 - upstream spec.** The vendor's truth about what the API can do (e.g. Forgejo's `swagger.v1.json`). - **L1 - overlay + `x-coily-*` extensions.** The compiled, deterministic policy IR. Standard OAI Overlay (JSON/YAML), targeting the spec by JSONPath. This is the interchange format other tooling (Speakeasy, oas-patch, libopenapi) understands, and it is what the runtime consumes. - **L2 - KDL Guardfile.** The human authoring layer. Pure data, schema-validated. Compiles down to L1. KDL replaces the *authoring* layer only. The overlay stays as the compiled artifact so the runtime targets a stable, pinned, hash-verified input. ## The runtime engine (`specverb`, lives in cli-guard) The generated Go we have today is ~90% static boilerplate (`doRequestAndStream`, `readBody`, `fetchSSM`) plus a tiny per-op descriptor (`{method, path, pathParams, queryParams, hasBody, authNeeded, verbName}`). A runtime engine builds `[]opDescriptor` from the parsed spec + overlay and binds them to one generic action. The thousands of lines of `*_generated.go` (and coily's Python `openapi-to-coily.py`) collapse into one tested engine. Maximally-generic form: embed the spec + overlay via `go:embed`, build the `cli.Command` tree at runtime (lazy/cached), mount under cli-guard's `verb.Wrap` + `audit.Writer`. **Deny-by-default allowlist:** an operation is mounted if and only if it carries an `x-coily-verb` annotation. The overlay is then purely additive (all `update` actions, no fragile `remove` sweeps), and the set of annotated ops *is* the allowlist. ### `x-coily-*` vocabulary (what the engine reads) - `x-coily-verb` - REQUIRED to mount. Value `"<group> <leaf>"`. Its presence is the allowlist. - `x-coily-args` - ordered positional args drawn from path params. - `x-coily-body-flags` - promote request-body scalar fields to typed flags (required schema fields -> required flags). Omit -> single raw `--body` (literal / `@file` / `-`). - `x-coily-fmt` - named `respfmt` hook for curated output. Absent -> pretty JSON. - `x-coily-resolve` - named pre-call hook for non-declarable cases (e.g. label name->id, the coily#159 behavior). - `x-coily-allow-redirect` - defaults false on mutating methods. Set true only to opt out of the 301 refusal (the coily#178 / #160 fix, now structural). ## The KDL Guardfile (L2 authoring) Chosen over a literal-Ruby internal DSL. A Ruby Guardfile would be `eval`'d - arbitrary code execution at build time, which is attack surface and is backwards for a tool (ward) whose job is to keep hostile code from running. **KDL is pure data**: parsing, not evaluation, so there is no code to smuggle in. It also ships a schema language (author-time validation) and a CSS-selector query language, and it is in-house (Kat Marchan's language). ```kdl wrap "coily ops forgejo" { spec "forgejo.swagger.v1.json" base-url "https://forgejo.coilysiren.me/api/v1" auth "header-token" header="Authorization" prefix="token " ssm="/forgejo/api-token" can "read" can "create" "edit" on="issues labels repos" // mounts `repo create` cannot "delete" except="label:created-by-me" never "force-push" } ``` Nodes are the modals (`can`/`cannot`/`never`), arguments are verb-classes, properties carry scope and exceptions. ## Security invariants (non-negotiable) - **Compiler, never interpreter.** KDL/LLM resolve to concrete DENY/APPROVE at build time. The compiled, pinned overlay is what enforces at runtime. No model is ever in the request path. - **Fail closed.** Anything the compiler cannot resolve to a concrete op or argv pattern with high confidence becomes DENY and surfaces for human annotation. - **Embed + pin by hash.** Spec + overlay are embedded and pinned, never live-fetched. A live spec would let upstream silently change the attack surface. - **Guardfile is maintainer-owned** (CODEOWNERS). For ward (public contrib), the reviewed artifact is the committed overlay diff. Contributors propose policy in English in the PR body. CI never evals an untrusted Guardfile. ## Two enforcement axes (the DSL authors both) - **Operation-level** (`cannot delete`) -> compiles to L1 overlay (mount/deny ops). The `x-coily` axis. - **Argument-value-level** (`secret get ssn => DENY`, `secret get login => APPROVE`) -> compiles to cli-guard's argv policy (`policy` / `scope` / `egress`). Value-level rules must lower to concrete identifiers/patterns, fail-closed on anything unmatched. The verb-class -> operation-set expansion (`delete` catching `TerminateInstances`, `RevokeGrant`, ...) is the one fuzzy step. It runs in the compiler with an LLM-assisted-or-curated, human-reviewed, committed expansion table - never at runtime. ## Findings from today's spec dig (Forgejo) - **Forgejo's spec is Swagger 2.0, not OpenAPI 3.x.** Body lives in `parameters[in:body]` (`requestBody` is null), security is global. The engine needs a 2.0 reader or a 2.0->3.x upgrade pass (kin-openapi `openapi2conv`). - **Auth is `Authorization: token <key>`, not `Bearer`.** `securityDefinitions.AuthorizationHeaderToken` requires the `token ` prefix. None of the existing generator auth modes cover this - the engine needs a declarable header-token scheme. - **`repo create` is genuinely missing today** and is the first proving slice: `POST /user/repos`, `operationId: createCurrentUserRepo`, body `CreateRepoOption` (required: `name`; scalars: `private`, `description`, `auto_init`, `default_branch`, ...). Org variant: `POST /orgs/{org}/repos` (`createOrgRepo`). ## First proving slice Drive `coily ops forgejo repo create` end-to-end through the chain: KDL Guardfile -> compile -> `forgejo.overlay.yaml` -> `specverb` builds the verb -> POST against the live Forgejo API with `token ` auth and body-flag assembly. Validates Swagger-2.0 read, the new auth scheme, deny-by-default mounting, and the 301 default in one shot. ## Open decisions 1. **Overlay apply timing.** (A) runtime apply (embed raw spec + overlay, apply in Go on first use, purest "Go handles it directly") vs (B) build-time apply, runtime mount (embed a pre-pruned spec, smaller runtime parse). Lean A with a lazy/cached build, fall back to B if Forgejo's large spec makes startup parse hurt. Benchmark before deciding. 2. **KDL parser.** `calico32/kdl-go` (v1+v2, passes upstream suite) keeps the compiler all-Go, vs Kat's reference Rust `kdl-rs`. Build-time only either way - never ships in the binary. 3. **Engine home.** `specverb` in cli-guard, consumed by ward/coily/kap. Confirm the KDL Guardfile vocabulary also ships from cli-guard (shared), with each CLI keeping its own Guardfile(s) under `cmd/`. ## Prior art - OpenAPI Overlay Specification v1.0.0 / latest (OAI). Speakeasy `openapi-overlay` (Go), pb33f libopenapi, `oas-patch` (Python). - KDL 2.0.0 (kdl.dev) - schema language, KQL query language. - kin-openapi `openapi2conv` for Swagger 2.0 -> OpenAPI 3.x. --- Filed by Claude during a design conversation with Kai. Companion to coily#186 and ward#51.
Author
Owner

Regenerated plan (2026-06-06)

Re-derived from scratch, then reconciled against the L0/L1/L2 + specverb design above. The architecture in this issue holds. What follows refines the engine home, consumer ordering, proving slice, and Guardfile UX, and lays out a milestone ladder.

Topology: cli-guard => ward first

  • Engine home: cli-guard. specverb (runtime engine) + a guardfile compiler land as new packages beside the existing verb / audit / policy / scope / egress / respfmt rails, reusing them.
  • First consumer: ward. ward already depends on cli-guard v0.3.0 and has zero forgejo verbs today, so forgejo-in-ward is greenfield. Nothing to migrate, nothing to break. This is the ward#51 CLI-argv surface.
  • Oracle is cross-repo. ward has no forgejo impl to sit adjacent to, but coily's hand-written ops forgejo repo create is the behavioral reference: ward's specverb-built verb must produce the same API call. coily migrates to specverb later (coily#186).

Iteration-loop friction to manage

Engine in cli-guard is a versioned dep ward pins. The fast dev loop uses a go.mod replace in ward pointing at the local cli-guard checkout for M0-M3, dropped and bumped to v0.4.0 before ward ships. Watch: pre-commit may flag a replace directive; tolerate or gate it in dev.

Proving slice: repo create + delete as a self-cleaning pair

Not load-bearing for automation, so safe to rebuild from scratch. As a pair they give a self-cleaning integration test: create a throwaway repo, assert, delete it, assert gone. create exercises body-flags + token auth + 301; delete exercises deny-by-default mount + destructive-confirm UX.

Fanatical UX is the thesis, not DRY

A generic engine is how you get uniform, excellent UX across every verb at once and one-sentence-to-add-a-verb iteration. The boilerplate collapse is a side effect. Written once in specverb, inherited everywhere: --dry-run (print resolved request, no fire), required-body-field -> required-flag with teaching errors, fail-closed denials that name the annotation that would lift them, --yes confirms on destructive ops, the respfmt --query/--output projection rail, generated help + examples.

Guardfile UX: flat declarative sentences

Policy body reads as plain English a vibe coder parses on sight. One fact per line, bare positional tokens, controlled vocabulary. Quotes quarantined to a write-once config header.

wrap ward ops forgejo {
    spec forgejo.swagger.v1.json
    base-url "https://forgejo.coilysiren.me/api/v1"
    auth header-token {
        header Authorization
        prefix "token "
        ssm "/forgejo/api-token"
    }

    can read repos
    can create repos
    can delete repos
}

Design rule: the daily-edited policy body must be expressible entirely in bare KDL tokens. Modals can/cannot/never, a verb set, a resource set. Exceptions go flat and positive (can delete labels created-by-me, not cannot delete except=label:created-by-me). Properties + child blocks stay a legal escape hatch off the happy path. An M2 lint fails the Guardfile if any token in a can/cannot/never subtree requires quoting.

This quietly collapses the NLP layer into the authoring layer: the Guardfile body is a controlled natural language, with KDL's parse-not-evaluate safety.

Milestone ladder

  • M0 (cli-guard) - specverb engine + guardfile compiler, thin enough to mount exactly one verb from a fixture spec, --dry-run working. Unit-tested in cli-guard, no ward yet. (KDL: a subset parser unblocks the engine now; the calico32/kdl-go swap + bare-token conformance check is the named follow-up, not a silent skip.)
  • M1 (ward, via replace) - ward embeds the forgejo Swagger 2.0 spec + a Guardfile with can create repos / can delete repos. specverb mounts ward ops forgejo repo create|delete. dry-run, then live, diffed against coily's hand-written verb. create+delete = self-cleaning integration test.
  • M2 - fanatical UX pass, uniform in specverb: flat-sentence lint, generated help/examples, required-flag errors, teaching denials, respfmt rail, --yes.
  • M3 - fan the Guardfile out to the forgejo surface ward actually needs. Proof of rapid iteration: new verb = one new sentence, zero Go.
  • M4 - cut cli-guard v0.4.0, drop ward's replace, bump ward. Migrate coily's hand-written verbs to specverb (coily#186) and embed-plus-pin the spec by hash.

Open decisions, resolved

  1. Overlay apply timing - runtime-apply for the proto, embed+pin deferred to M4.

  2. KDL parser - calico32/kdl-go, build-time only, swapped in behind guardfile.Parse (subset parser bridges M0).

  3. Engine home - cli-guard, prototyped with ward as first consumer; coily migrates at M4.

  4. Overlay tooling - Speakeasy openapi-overlay (Go) is the L1 apply library. The KDL Guardfile (L2) compiles to a standard OpenAPI Overlay doc carrying the x-cli-guard-* extensions; Speakeasy applies it onto L0 to yield the annotated spec the engine mounts from. No hand-rolled overlay application.

    Extension prefix: x-cli-guard- (not the issue's original x-coily-*). The engine lives in cli-guard and is consumed by ward/coily/kap, so the vocabulary carries the engine's name, not one consumer's. So x-cli-guard-verb (REQUIRED to mount, presence is the allowlist), x-cli-guard-args, x-cli-guard-body-flags, x-cli-guard-fmt, x-cli-guard-resolve, x-cli-guard-allow-redirect.

Still open: which forgejo surface ward genuinely needs (aims M3).

## Regenerated plan (2026-06-06) Re-derived from scratch, then reconciled against the L0/L1/L2 + specverb design above. The architecture in this issue holds. What follows refines the **engine home, consumer ordering, proving slice, and Guardfile UX**, and lays out a milestone ladder. ### Topology: cli-guard => ward first - **Engine home: cli-guard.** `specverb` (runtime engine) + a `guardfile` compiler land as new packages beside the existing `verb` / `audit` / `policy` / `scope` / `egress` / `respfmt` rails, reusing them. - **First consumer: ward.** ward already depends on `cli-guard v0.3.0` and has **zero forgejo verbs today**, so forgejo-in-ward is greenfield. Nothing to migrate, nothing to break. This is the `ward#51` CLI-argv surface. - **Oracle is cross-repo.** ward has no forgejo impl to sit adjacent to, but coily's hand-written `ops forgejo repo create` is the behavioral reference: ward's specverb-built verb must produce the same API call. coily migrates to specverb later (`coily#186`). ### Iteration-loop friction to manage Engine in cli-guard is a versioned dep ward pins. The fast dev loop uses a `go.mod replace` in ward pointing at the local cli-guard checkout for M0-M3, dropped and bumped to `v0.4.0` before ward ships. Watch: pre-commit may flag a `replace` directive; tolerate or gate it in dev. ### Proving slice: repo create + delete as a self-cleaning pair Not load-bearing for automation, so safe to rebuild from scratch. As a **pair** they give a self-cleaning integration test: create a throwaway repo, assert, delete it, assert gone. create exercises body-flags + token auth + 301; delete exercises deny-by-default mount + destructive-confirm UX. ### Fanatical UX is the thesis, not DRY A generic engine is how you get **uniform, excellent UX across every verb at once** and **one-sentence-to-add-a-verb iteration**. The boilerplate collapse is a side effect. Written once in specverb, inherited everywhere: `--dry-run` (print resolved request, no fire), required-body-field -> required-flag with teaching errors, fail-closed denials that name the annotation that would lift them, `--yes` confirms on destructive ops, the `respfmt` `--query`/`--output` projection rail, generated help + examples. ### Guardfile UX: flat declarative sentences Policy body reads as plain English a vibe coder parses on sight. One fact per line, bare positional tokens, controlled vocabulary. Quotes quarantined to a write-once config header. ```kdl wrap ward ops forgejo { spec forgejo.swagger.v1.json base-url "https://forgejo.coilysiren.me/api/v1" auth header-token { header Authorization prefix "token " ssm "/forgejo/api-token" } can read repos can create repos can delete repos } ``` Design rule: **the daily-edited policy body must be expressible entirely in bare KDL tokens.** Modals `can`/`cannot`/`never`, a verb set, a resource set. Exceptions go flat and positive (`can delete labels created-by-me`, not `cannot delete except=label:created-by-me`). Properties + child blocks stay a legal escape hatch off the happy path. An M2 lint fails the Guardfile if any token in a `can`/`cannot`/`never` subtree requires quoting. This quietly collapses the NLP layer into the authoring layer: the Guardfile body *is* a controlled natural language, with KDL's parse-not-evaluate safety. ### Milestone ladder - **M0 (cli-guard)** - `specverb` engine + `guardfile` compiler, thin enough to mount exactly one verb from a fixture spec, `--dry-run` working. Unit-tested in cli-guard, no ward yet. (KDL: a subset parser unblocks the engine now; the `calico32/kdl-go` swap + bare-token conformance check is the named follow-up, not a silent skip.) - **M1 (ward, via replace)** - ward embeds the forgejo Swagger 2.0 spec + a Guardfile with `can create repos` / `can delete repos`. specverb mounts `ward ops forgejo repo create|delete`. dry-run, then live, diffed against coily's hand-written verb. create+delete = self-cleaning integration test. - **M2** - fanatical UX pass, uniform in specverb: flat-sentence lint, generated help/examples, required-flag errors, teaching denials, `respfmt` rail, `--yes`. - **M3** - fan the Guardfile out to the forgejo surface ward actually needs. Proof of rapid iteration: new verb = one new sentence, zero Go. - **M4** - cut cli-guard `v0.4.0`, drop ward's replace, bump ward. Migrate coily's hand-written verbs to specverb (`coily#186`) and embed-plus-pin the spec by hash. ### Open decisions, resolved 1. **Overlay apply timing** - runtime-apply for the proto, embed+pin deferred to M4. 2. **KDL parser** - `calico32/kdl-go`, build-time only, swapped in behind `guardfile.Parse` (subset parser bridges M0). 3. **Engine home** - cli-guard, prototyped with ward as first consumer; coily migrates at M4. 4. **Overlay tooling** - Speakeasy `openapi-overlay` (Go) is the L1 apply library. The KDL Guardfile (L2) compiles to a standard OpenAPI Overlay doc carrying the `x-cli-guard-*` extensions; Speakeasy applies it onto L0 to yield the annotated spec the engine mounts from. No hand-rolled overlay application. **Extension prefix: `x-cli-guard-`** (not the issue's original `x-coily-*`). The engine lives in cli-guard and is consumed by ward/coily/kap, so the vocabulary carries the engine's name, not one consumer's. So `x-cli-guard-verb` (REQUIRED to mount, presence is the allowlist), `x-cli-guard-args`, `x-cli-guard-body-flags`, `x-cli-guard-fmt`, `x-cli-guard-resolve`, `x-cli-guard-allow-redirect`. Still open: which forgejo surface ward genuinely needs (aims M3).
Author
Owner

Session checkpoint: M0 status + specverb design notes

Handoff so a fresh session starts at the engine, not by re-deriving. Supersedes the prior comment's "subset parser bridges M0, kdl-go is a follow-up" note.

Done this session (landed in cli-guard, make test/make vet green)

  • guardfile package parses the flat-sentence KDL Guardfile into a typed model (Group, Spec, BaseURL, Auth, Grants). Fail-closed on unknown nodes, missing required fields, malformed sentences, and unsupported auth schemes.
  • Built against real calico32/kdl-go v0.14.1, not a stub.
  • Bare-token question answered empirically. A test asserts the dotted forgejo.swagger.v1.json and the flat can delete labels created-by-me parse as bare identifiers. Only / and the trailing-space prefix "token " force quotes, and those are exactly the config-header tokens. The "policy body stays quote-free" rule is confirmed by the parser.
  • speakeasy-api/openapi-overlay v0.10.3 confirmed resolvable.
  • Files: guardfile/guardfile.go, guardfile/guardfile_test.go, guardfile/testdata/forgejo.kdl, plus go.mod/go.sum.

Next: the specverb engine (design decided this session)

  • Spec reader, minimal for M0. Read only what the proving op needs from the Swagger 2.0 spec (path, method, operationId, body params). Full kin-openapi openapi2conv Swagger-2.0 -> 3.x is M1, not M0.
  • Expansion table, curated + committed. (verb, resource) -> {cliGroup, cliLeaf, operationId}, e.g. {create, repos} -> {repo, create, createCurrentUserRepo}. Deny-by-default: no entry, no mount. This is the human-reviewed table the issue calls for.
  • Generic action. Builds the HTTP request from the operation descriptor + flags. --dry-run prints the resolved request (method, URL, headers, body) without firing. Live path fires then runs respfmt.Render for the --query/--output rail.
  • TokenResolver injection. specverb takes a func(ctx, ssmPath) (string, error) so the AWS SDK stays OUT of cli-guard (repo-boundary rule: no consumer-shaped deps leak in). ward wires the real SSM resolver; tests inject a fake.
  • Command-tree mapping. Guardfile group (["ward","ops","forgejo"]) -> tree root; resource (plural) -> cli group (singular noun); verb -> cli leaf. specverb.Build returns the leaf group command the consumer mounts.
  • Overlay pipeline. guardfile model -> OpenAPI Overlay doc with x-cli-guard-* extensions -> Speakeasy applies onto L0 -> engine mounts ops carrying x-cli-guard-verb.

Repo facts for the next session

  • cli-guard is deliberately unguarded: dev verbs run through make (make test/vet/tidy), not bare go.
  • Commits close a same-repo issue; pre-commit pins godoc-current.txt (regen on API change via scripts/check-godoc-current.sh --update).
  • The engine dev loop with ward uses a go.mod replace to local cli-guard for M0-M3, dropped and tagged v0.4.0 at M4.
## Session checkpoint: M0 status + specverb design notes Handoff so a fresh session starts at the engine, not by re-deriving. Supersedes the prior comment's "subset parser bridges M0, kdl-go is a follow-up" note. ### Done this session (landed in cli-guard, `make test`/`make vet` green) - **`guardfile` package** parses the flat-sentence KDL Guardfile into a typed model (`Group`, `Spec`, `BaseURL`, `Auth`, `Grants`). Fail-closed on unknown nodes, missing required fields, malformed sentences, and unsupported auth schemes. - Built against **real `calico32/kdl-go v0.14.1`**, not a stub. - **Bare-token question answered empirically.** A test asserts the dotted `forgejo.swagger.v1.json` and the flat `can delete labels created-by-me` parse as bare identifiers. Only `/` and the trailing-space `prefix "token "` force quotes, and those are exactly the config-header tokens. The "policy body stays quote-free" rule is confirmed by the parser. - **`speakeasy-api/openapi-overlay v0.10.3`** confirmed resolvable. - Files: `guardfile/guardfile.go`, `guardfile/guardfile_test.go`, `guardfile/testdata/forgejo.kdl`, plus `go.mod`/`go.sum`. ### Next: the `specverb` engine (design decided this session) - **Spec reader, minimal for M0.** Read only what the proving op needs from the Swagger 2.0 spec (path, method, operationId, body params). Full kin-openapi `openapi2conv` Swagger-2.0 -> 3.x is M1, not M0. - **Expansion table, curated + committed.** `(verb, resource) -> {cliGroup, cliLeaf, operationId}`, e.g. `{create, repos} -> {repo, create, createCurrentUserRepo}`. Deny-by-default: no entry, no mount. This is the human-reviewed table the issue calls for. - **Generic action.** Builds the HTTP request from the operation descriptor + flags. `--dry-run` prints the resolved request (method, URL, headers, body) without firing. Live path fires then runs `respfmt.Render` for the `--query`/`--output` rail. - **`TokenResolver` injection.** specverb takes a `func(ctx, ssmPath) (string, error)` so the AWS SDK stays OUT of cli-guard (repo-boundary rule: no consumer-shaped deps leak in). ward wires the real SSM resolver; tests inject a fake. - **Command-tree mapping.** Guardfile group (`["ward","ops","forgejo"]`) -> tree root; resource (plural) -> cli group (singular noun); verb -> cli leaf. `specverb.Build` returns the leaf group command the consumer mounts. - **Overlay pipeline.** guardfile model -> OpenAPI Overlay doc with `x-cli-guard-*` extensions -> Speakeasy applies onto L0 -> engine mounts ops carrying `x-cli-guard-verb`. ### Repo facts for the next session - cli-guard is deliberately unguarded: dev verbs run through `make` (`make test`/`vet`/`tidy`), not bare go. - Commits close a same-repo issue; pre-commit pins `godoc-current.txt` (regen on API change via `scripts/check-godoc-current.sh --update`). - The engine dev loop with ward uses a `go.mod replace` to local cli-guard for M0-M3, dropped and tagged `v0.4.0` at M4.
coilysiren added
P3
and removed
P2
labels 2026-06-17 08:39:53 +00:00
Member

🛫 ward pre-flight: NO-GO

ward agent claude headless ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it NO-GO - it should not be carried unattended until a human weighs in.

open-ended multi-milestone epic with open design decisions and live-API/secret dependencies; no bounded, container-verifiable done-condition for one unattended run.

No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - ward agent claude headless <ref> --no-preflight skips this gate once you've decided it's good to go.

full pre-flight read

This issue is a multi-milestone design epic (M0–M4) spanning cli-guard, ward, and coily, with explicitly-open decisions ("which forgejo surface ward genuinely needs" still open) and no single bounded done-condition — the M0 slice alone wants a Swagger-2.0 reader, a curated expansion table, Speakeasy overlay wiring, and a generic action, and the proving slice depends on live Forgejo API calls with token auth (SSM secrets + network) that an ephemeral container can't exercise or verify. The guardfile package already landed per the latest checkpoint, so the home is right — but "carry to merge unattended" has no crisp target here without a human picking the exact slice. This needs scoping, not autonomous execution.

NO-GO: open-ended multi-milestone epic with open design decisions and live-API/secret dependencies; no bounded, container-verifiable done-condition for one unattended run.


Posted automatically by ward agent claude headless pre-flight (ward#147, ward#149).

— Claude (she/her), via ward agent

### 🛫 ward pre-flight: NO-GO `ward agent claude headless` ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it **NO-GO** - it should not be carried unattended until a human weighs in. > open-ended multi-milestone epic with open design decisions and live-API/secret dependencies; no bounded, container-verifiable done-condition for one unattended run. No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - `ward agent claude headless <ref> --no-preflight` skips this gate once you've decided it's good to go. <details><summary>full pre-flight read</summary> This issue is a multi-milestone design epic (M0–M4) spanning cli-guard, ward, and coily, with explicitly-open decisions ("which forgejo surface ward genuinely needs" still open) and no single bounded done-condition — the M0 slice alone wants a Swagger-2.0 reader, a curated expansion table, Speakeasy overlay wiring, and a generic action, and the proving slice depends on live Forgejo API calls with `token` auth (SSM secrets + network) that an ephemeral container can't exercise or verify. The guardfile package already landed per the latest checkpoint, so the home is right — but "carry to merge unattended" has no crisp target here without a human picking the exact slice. This needs scoping, not autonomous execution. NO-GO: open-ended multi-milestone epic with open design decisions and live-API/secret dependencies; no bounded, container-verifiable done-condition for one unattended run. </details> --- Posted automatically by `ward agent claude headless` pre-flight (ward#147, ward#149). <!-- ward-preflight-nogo --> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

this is mostly done already actually

this is mostly done already actually
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#75
No description provided.