design: inline operation authoring - the .mcp.kdl defines the surface, drop the vendored OpenAPI as a build input #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Decision (Kai, 2026-07-07)
Inline everywhere. A
.mcp.kdldefines its operations inline - method, path-template, typed params - and is the whole contract. The vendored OpenAPI JSON, its.lock.json, the prune step, andoppins are dropped as build inputs. Where a real upstream OpenAPI exists (forgejo's swagger), it's a one-time authoring reference to copy from, never something the build consumes.This amends docs/DESIGN.md, whose pipeline today bakes
guardfile + <spec>.lock.json + specverb.lock + runtimeand derives schemas "from the OpenAPI operation." Under inline authoring the schema is derived from the inline op definition, and the two locks leave the build.Why (the pressure test that led here)
The strict structure was CLI-era discipline for a shipped binary wrapping an upstream in reproducible offline builds. None of its load-bearing reasons survive the move to a live MCP:
$reflock = offline-build determinism. An MCP image carries the surface directly and the guard denies everything ungranted anyway, so pruning is an optional size optimization, not correctness..lock.json= freeze a fetched upstream against drift. skillsmp / glama / trello have no upstream OpenAPI - the file is hand-authored, already the git-tracked source of truth, so the lock is redundant with git.oppins = bind a grant to an operationId. Inline ops carry{method, path, params}directly, so there is no operationId to resolve or override - the concept evaporates. (The resolver nit that makesopneedless for the spec-backed CLI path is filed in cli-guard separately; inline sidesteps it entirely.)The live API is the truth and does not care about our versioning, so freezing a copy of its schema buys little for a surface we call live. What the strictness did buy - a fixed, reviewable surface - inline authoring keeps and improves: skillsmp collapses from three files (guardfile + openapi.json + lock) to one, which is exactly ward-mcp's safety pitch ("audit one small file, know the blast radius").
The grammar (sketch, to be settled here)
A tool defined inline, no
spec, noop:Params carry their location (path / query / body) and type. That metadata is irreducible - request assembly and the metachar / restrict gates need it - it just moves from OpenAPI JSON into KDL.
restrict/never/can-vs-toolpolicy semantics are unchanged; aneverstill means the tool is never minted.The enabling engine capability (the real cost)
cli-guard's
specverbbuilds operation descriptors from an OpenAPI spec (L0). Inline authoring needs a descriptor built from inline KDL instead. Picking how is the key design fork:execverb(inline KDL, no spec), so this extends the "author the surface inline" pattern to HTTP ops. Keeps ward-mcp a thin driver and the safety engine shared. Preferred, per the layering law (cli-guard is the engine).Recommend (a); split a cli-guard issue for the inline-op source once this grammar settles.
Guardrails (so "more flex" does not overshoot)
Rollout
docs/DESIGN.md.specverb(option a)..mcp.kdl, project one MCP tool pertool/can, derive the MCP inputSchema from the inline params.Related: deploy#60 (the strict skillsmp, the "before"), the cli-guard resolver nit (spec-backed path), coilysiren/inbox#164 (the runtime),
docs/DESIGN.md(to amend). The safety model, HTTP/SSE transport, and interior-only scope from DESIGN.md are unchanged - only the operation-source layer changes.Engine fork resolved: option (a), pushed as far down as it goes. Kai's directive - push as much impl into cli-guard as possible; ward-mcp stays a thin driver. Same discipline ward#265 is unwinding for ward, applied to ward-mcp from birth so it never grows the bloat.
Engine home filed as cli-guard#196 (transport-neutral core + inline-operation source). The boundary:
toolblock -> operation descriptor; expose the op's input JSON-schema; guarded-execute (validate -> restrict/metachar gates -> sign + fire the HTTP request). The inline-op source sits beside the existing OpenAPI source;execverbis the precedent (inline KDL, no spec).So DESIGN.md's "grant -> MCP tool projection (op descriptor -> JSON-schema + handler)" splits: the descriptor + JSON-schema half comes down into cli-guard (shared with the CLI projection), ward-mcp keeps only the MCP-tool envelope + transport. Respects cli-guard#190 - no orchestration comes down, only the engine core.
This issue now owns the inline
.mcp.kdlgrammar design (thetoolblock shape); cli-guard#196 owns the engine that consumes it. They co-design, then cli-guard#196 implements the source and ward-mcp's runtime is a thin shell over it.Inline
.mcp.kdlgrammar + distribution - co-designed with Kai, 2026-07-07Locks the two halves ward-mcp#6 opened. Amends DESIGN.md. The engine that consumes this is cli-guard#196.
Grammar
Extend the existing
can <verb> <resource>grant - not a newtoolnode - so one grammar carries both op-sources (spec-backed and inline) andrestrict/never/inherit/action/ tool-naming keep working. The inline body states the descriptor the spec used to resolve.Decided rules
specverb-resolution.md). An unconventional verb states an explicit method.{name}tokens as required strings; an optionalpath <name> <type>line refines type/description.location(path/query/body/header) is request-assembly metadata only, not input shape.neversurvives only to override an inherited grant.auth,restrict,describe,action/call,inherit/override can.Worked: skillsmp (the simplest)
Worked: forgejo (path params, body, restrict, fixed-body, deny, compose)
Distribution - generic runtime + Helm chart (ward-mcp ships both)
Retire per-guardfile image bake. One generic
ward-mcpruntime image (cli-guard engine + SSE/HTTP server) serves any mounted.mcp.kdl. ward-mcp ships a Helm chart that templates Deployment (runtime + the guardfile as a ConfigMap mount) + Service + Authelia route + token Secret. The.mcp.kdlis chart values; deploy supplies per-MCP values.DESIGN.md amendments
guardfile → ward mcp build → OCI image → deploybecomesguardfile = chart values → helm → pod mounting the one runtime image. "Landing the guardfile IS publishing the image" becomes "landing the guardfile IS a values change /helm upgrade."ward mcp buildrepurposes from image-bake to an optional local test-serve..mcp.kdl, now a ConfigMap rather than an image digest.Ripples
.mcp.kdl, not the forgejo-mcp per-service manifest layout..mcp.kdl(deploy#46), deployed via the chart.🔒 Reserved by
ward agent --harness codex— containerengineer-codex-ward-mcp-6on hostkais-macbook-pro-2.localis carrying this issue (reserved 2026-07-08T17:09:06Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.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)
coilyco-flight-deck/ward-mcp#6· branchissue-6· harnesscodex· workflowdirect-mainengineer-codex-ward-mcp-6· wardv0.451.0· dispatched2026-07-08T17:09:06ZIssue body as seeded:
… (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.451.0).
— Codex, via
ward agentWARD-OUTCOME: done - documented inline .mcp.kdl authoring as the whole contract and removed the stale OpenAPI build-input story
PR-BODY-NOTE: ADVISORY-ONLY REVIEW: no heterogeneous reviewer family was available besides the worker (codex), so the adversarial panel could not run and did NOT gate this diff. Dropped: opencode (unavailable: opencode not on PATH); codex (worker's own family - never reviews its own diff). A human should review this change with the extra scrutiny an unrun panel would have applied.
This felt mostly like documentation cleanup because the runtime already lived on the inline path. The only friction was reconciling the remote
mainupdate before landing, which widened the merge a bit, but the final shape is still straightforward and I am confident in it.