fix(ops): a (placeholder) sentinel must never win the guardfile merge #1123

Merged
coilyco-ops merged 1 commit from fix/placeholder-sentinels into main 2026-07-12 02:27:36 +00:00
Member

Implements Kai's design (ward#1122). Clears the last ward doctor placeholder failure against the aos bundle.

The bug

ward must never embed real deployment config, so its generic guardfiles have to name some host, token, and owner gate. Those values are sentinels - they exist so ward doctor fails a deployment that never supplied its own. That intent is right.

But they were indistinguishable from real config, and cli-guard's singletons are last-wins (applyBaseURL keeps the last non-empty; gf.Auth = a overwrites). The bundle merge concatenates children across files, so whichever file the walk reached last decided the surface.

On the coilyco bundle that produced a split result, which I measured two ways:

  • ward doctor resolved the effective base-url to git.example.com/api/v1 (the sentinel, from ops.forgejo.kdl)
  • runtime ward ops forgejo get repo ... actually requested ssm get-parameter --name /forgejo/coilyco-ops/api-token (the real token, from guardfile.forgejo.kdl)

So the merged ops surface paired the example host with the live credential. A sentinel that can win a merge is worse than no sentinel at all.

The fix

A node may carry a (placeholder) type annotation. The merge drops any sentinel that a real sibling of the same name supplies - whatever the order. A sentinel with nothing to override it survives, so doctor still fails on it, which is the behavior worth keeping.

(placeholder)base-url "git.example.com/api/v1"
(placeholder)auth header-token { ... }
(placeholder)restrict owner matches example*

No cli-guard change needed: ward reads the annotation off the KDL node during the merge, before cli-guard's parser sees it. Pairs with the aos side, which marks the sentinels.

Verification

Inside agentic-os:latest: go build / go vet clean, go test ./... exit 0, golangci-lint 0 issues. New tests cover both merge orders, the unopposed sentinel surviving, and unmarked duplicates being left alone for cli-guard to catch.

Against the annotated aos bundle, doctor now reports PASS smart defaults, PASS repo authority, PASS fleet, PASS exec bundle, and the real coilyco base-url wins.

What it unmasks

One failure remains, and it is pre-existing, not caused here - the placeholder failure was short-circuiting validateGuardfileOperational before buildForgejoOps ever ran:

FAIL ops bundle: specverb: operationId "repoGetCombinedStatusByRef" not found in spec

agentic-os#446 added the commit-status verbs to guardfile.forgejo.kdl, but .ward/forgejo.swagger.lock.json was never regenerated - it carries 47 operationIds and zero status ops. The lock needs a regen (make lock / the ward-kdl driver's lock --guardfile). Tracked in aos#452.

Implements Kai's design (ward#1122). Clears the **last** `ward doctor` placeholder failure against the aos bundle. ## The bug ward must never embed real deployment config, so its generic guardfiles have to name *some* host, token, and owner gate. Those values are **sentinels** - they exist so `ward doctor` fails a deployment that never supplied its own. That intent is right. But they were indistinguishable from real config, and cli-guard's singletons are **last-wins** (`applyBaseURL` keeps the last non-empty; `gf.Auth = a` overwrites). The bundle merge concatenates children across files, so whichever file the walk reached last decided the surface. On the coilyco bundle that produced a **split** result, which I measured two ways: * `ward doctor` resolved the effective `base-url` to **`git.example.com/api/v1`** (the sentinel, from `ops.forgejo.kdl`) * runtime `ward ops forgejo get repo ...` actually requested **`ssm get-parameter --name /forgejo/coilyco-ops/api-token`** (the **real** token, from `guardfile.forgejo.kdl`) So the merged ops surface paired the **example host** with the **live credential**. A sentinel that can win a merge is worse than no sentinel at all. ## The fix A node may carry a `(placeholder)` type annotation. The merge drops any sentinel that a real sibling of the same name supplies - **whatever the order**. A sentinel with nothing to override it **survives**, so doctor still fails on it, which is the behavior worth keeping. ```kdl (placeholder)base-url "git.example.com/api/v1" (placeholder)auth header-token { ... } (placeholder)restrict owner matches example* ``` No cli-guard change needed: ward reads the annotation off the KDL node during the merge, before cli-guard's parser sees it. Pairs with the aos side, which marks the sentinels. ## Verification Inside `agentic-os:latest`: `go build` / `go vet` clean, `go test ./...` **exit 0**, `golangci-lint` **0 issues**. New tests cover both merge orders, the unopposed sentinel surviving, and unmarked duplicates being left alone for cli-guard to catch. Against the annotated aos bundle, doctor now reports `PASS smart defaults`, `PASS repo authority`, `PASS fleet`, `PASS exec bundle`, and the real coilyco base-url wins. ## What it unmasks One failure remains, and it is **pre-existing**, not caused here - the placeholder failure was short-circuiting `validateGuardfileOperational` before `buildForgejoOps` ever ran: ``` FAIL ops bundle: specverb: operationId "repoGetCombinedStatusByRef" not found in spec ``` `agentic-os#446` added the commit-status verbs to `guardfile.forgejo.kdl`, but `.ward/forgejo.swagger.lock.json` was never regenerated - it carries 47 operationIds and **zero** status ops. The lock needs a regen (`make lock` / the ward-kdl driver's `lock --guardfile`). Tracked in aos#452.
fix(ops): a (placeholder) sentinel must never win the guardfile merge
All checks were successful
test / test (pull_request) Successful in 30s
fbbc632031
ward must never embed real deployment config, so its generic guardfiles have to
name some host, token, and owner gate. Those values are sentinels: they exist so
ward doctor fails a deployment that never supplied its own.

They were indistinguishable from real config, and cli-guard singletons are
last-wins (applyBaseURL keeps the last non-empty, gf.Auth = a overwrites). The
bundle merge concatenates children across files, so whichever file the walk
reached last decided the surface. On the coilyco bundle that produced a split
result: the sentinel base-url from the generic ops guardfile won, while the auth
from the deployment guardfile also won. The merged ops surface paired
git.example.com with the live /forgejo/coilyco-ops/api-token. A sentinel that can
win a merge is worse than no sentinel at all.

A node may now carry a (placeholder) type annotation, and the merge drops any
sentinel that a real sibling of the same name supplies, whatever the order. A
sentinel with nothing to override it survives, so doctor still fails on it, which
is the behavior worth keeping.

This clears `FAIL ops bundle: guardfile base-url "git.example.com/api/v1" still
looks like a placeholder` against the aos bundle, and the real coilyco base-url
now wins. It also unmasks a pre-existing stale spec lock (the commit-status ops
from agentic-os#446 were never locked), which the placeholder failure had been
short-circuiting before buildForgejoOps ever ran.

Refs ward#1122, ward#1120, aos#452.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.