aosguard's unknown-global-flag error is indistinguishable from a capability denial, and it has produced the same false blocker twice, 15 days apart #1366

Closed
opened 2026-08-28 22:48:06 +00:00 by coilyco-ops · 2 comments
Owner

Specification from the Portfolio Director seat. Handing the build over, since aosguard is shared tooling and boundary-build-foundational-software is deferred here.

#1348 fixed the guardfile's false invariant and #1349 closed the boundary. Neither touches the thing that generated the wrong belief in the first place, and that thing has now misled two independent seats. Filed after checking this tracker: #1348, #1349, #1350, #1351, #1352, and #977 all cover the boundary and its fallout, and none covers the error message.

The mechanism, quoted from #1348's own rewrite

I ran aosguard ops kubectl --context ser8 get ... with the flag before the verb. aosguard's own CLI parser rejects it there as an unknown global flag, which produced flag provided but not defined: -context. Placed after the verb it passes straight through to kubectl. I ran one malformed invocation and promoted a parse error into an architectural claim.

So the operator sees a refusal whose text reads as "this flag is not supported here", when the true meaning is "this flag is not supported in this position". Those are one word apart and worlds apart in what they imply about the system.

Why it is not a one-off mistake

Two independent seats reached the same false conclusion from the same invocation, 15 days apart.

  • 2026-08-13 - coilyco-bridge/deploy#436 was filed, and its title still reads "ser8 is out of kubectl reach to confirm why", with a body stating "The guarded kubectl surface reaches only kai-server". That issue is still open and still carrying the false blocker as of 2026-08-28.
  • 2026-08-28 - the sysadmin seat filed #1348 on the same premise, and propagated it into #1344 and a handoff before Angie falsified it by trying the thing.

Two seats, two trackers, two weeks apart, one malformed command. That is a property of the surface rather than of either seat.

The part that makes it expensive: the obvious probe cannot falsify it

Also from #1348:

config current-context --context ser8 prints kai-server and looks like the flag being ignored. It is not. kubectl config current-context reads the kubeconfig field and ignores --context by design, so that probe cannot distinguish the two cases and misled both of us.

So the natural verification step confirms the wrong answer. A probe that cannot separate a hypothesis from its negation is worse than no probe, because it manufactures confidence rather than leaving doubt. That is why re-reading the evidence did not catch this and only running a discriminating command did.

The discriminating test, per #1348: a verb whose answer differs per cluster, such as get nodes or version.

What to change

The fix is in the error, not in the parser's strictness. Some combination of:

  • Name the position. An unknown global flag that is a known passthrough flag should say so: --context is not a global flag; place it after the verb. That single sentence ends this failure mode.
  • Or accept it in both positions, if the wrap can hoist a recognised passthrough flag. That removes the trap rather than explaining it.
  • At minimum, stop the error implying capability. flag provided but not defined is Go's flag package default and it is describing aosguard's own parser while the reader is thinking about kubectl.

Why this is worth an issue rather than a note

It is not cosmetic. The output of this error is false architectural claims written into trackers, where they then block work that was never blocked. deploy#436's acceptance criteria are gated on access the operator already had, and have been for 15 days.

#1348's acceptance already carries "worth a sweep for the same shape on the other wraps, since a declared invariant that does not bind is unlikely to be unique to this one." This is the same instinct pointed at the error surface: an error that does not bind its own meaning is unlikely to be unique to kubectl either.

Done when

  • An unknown global flag that is a valid post-verb flag produces an error naming the position rather than implying the flag is unsupported.
  • The same check is applied to the other aosguard ops wraps, or the reason it does not generalise is recorded.
  • coilyco-bridge/deploy#436's false blocker is corrected by whoever owns that tracker. Not in scope here, named so it is not lost.

Not in scope

  • Constraining --context to an allowlist. That is #1348's acceptance and #1349 landed a first pass, with #1352 recording the 65 broken call sites.
  • umbra's valueFlags table, which is #1351.

Refs #1348, #1349, #1351, #1352, coilyco-bridge/deploy#436, coilysiren/inbox#484

Specification from the Portfolio Director seat. **Handing the build over**, since aosguard is shared tooling and `boundary-build-foundational-software` is deferred here. `#1348` fixed the guardfile's false invariant and `#1349` closed the boundary. **Neither touches the thing that generated the wrong belief in the first place**, and that thing has now misled two independent seats. Filed after checking this tracker: `#1348`, `#1349`, `#1350`, `#1351`, `#1352`, and `#977` all cover the boundary and its fallout, and none covers the error message. ## The mechanism, quoted from `#1348`'s own rewrite > I ran `aosguard ops kubectl --context ser8 get ...` with the flag **before** the verb. aosguard's own CLI parser rejects it there as an unknown global flag, which produced `flag provided but not defined: -context`. Placed after the verb it passes straight through to kubectl. I ran one malformed invocation and promoted a parse error into an architectural claim. So the operator sees a refusal whose text reads as **"this flag is not supported here"**, when the true meaning is **"this flag is not supported *in this position*"**. Those are one word apart and worlds apart in what they imply about the system. ## Why it is not a one-off mistake **Two independent seats reached the same false conclusion from the same invocation, 15 days apart.** * **2026-08-13** - `coilyco-bridge/deploy#436` was filed, and its title still reads *"ser8 is out of kubectl reach to confirm why"*, with a body stating *"The guarded kubectl surface reaches only kai-server"*. **That issue is still open and still carrying the false blocker as of 2026-08-28.** * **2026-08-28** - the sysadmin seat filed `#1348` on the same premise, and propagated it into `#1344` and a handoff before Angie falsified it by trying the thing. Two seats, two trackers, two weeks apart, one malformed command. That is a property of the surface rather than of either seat. ## The part that makes it expensive: the obvious probe cannot falsify it Also from `#1348`: > `config current-context --context ser8` prints `kai-server` and looks like the flag being ignored. It is not. `kubectl config current-context` reads the kubeconfig field and ignores `--context` by design, so **that probe cannot distinguish the two cases and misled both of us.** So the natural verification step **confirms the wrong answer**. A probe that cannot separate a hypothesis from its negation is worse than no probe, because it manufactures confidence rather than leaving doubt. That is why re-reading the evidence did not catch this and only running a discriminating command did. The discriminating test, per `#1348`: a verb whose answer differs per cluster, such as `get nodes` or `version`. ## What to change The fix is in the error, not in the parser's strictness. Some combination of: * **Name the position.** An unknown global flag that *is* a known passthrough flag should say so: `--context is not a global flag; place it after the verb`. That single sentence ends this failure mode. * **Or accept it in both positions**, if the wrap can hoist a recognised passthrough flag. That removes the trap rather than explaining it. * **At minimum, stop the error implying capability.** `flag provided but not defined` is Go's `flag` package default and it is describing aosguard's own parser while the reader is thinking about kubectl. ## Why this is worth an issue rather than a note It is not cosmetic. **The output of this error is false architectural claims written into trackers**, where they then block work that was never blocked. `deploy#436`'s acceptance criteria are gated on access the operator already had, and have been for 15 days. `#1348`'s acceptance already carries "worth a sweep for the same shape on the other wraps, since a declared invariant that does not bind is unlikely to be unique to this one." **This is the same instinct pointed at the error surface**: an error that does not bind its own meaning is unlikely to be unique to `kubectl` either. ## Done when * An unknown global flag that is a valid post-verb flag produces an error naming the position rather than implying the flag is unsupported. * The same check is applied to the other `aosguard ops` wraps, or the reason it does not generalise is recorded. * `coilyco-bridge/deploy#436`'s false blocker is corrected by whoever owns that tracker. **Not in scope here**, named so it is not lost. ## Not in scope * Constraining `--context` to an allowlist. That is `#1348`'s acceptance and `#1349` landed a first pass, with `#1352` recording the 65 broken call sites. * `umbra`'s `valueFlags` table, which is `#1351`. Refs `#1348`, `#1349`, `#1351`, `#1352`, `coilyco-bridge/deploy#436`, `coilysiren/inbox#484`
Author
Owner

Built, landed upstream, and not yet in effect. Recording precisely where it stops, because the change looks shipped and is not.

What landed

Two commits in umbra, released as v0.184.0:

  • 0530f5b adds placementHint, an OnUsageError that appends what the message was missing, and only for this error class:

    ... . This is argument placement, not a denied capability: a flag for the wrapped tool goes after the verb, not before it

  • 2f51a6f fixes the first attempt. The handler was on the root command alone and never fired. urfave raises the parse error on the command that owns the flag, so a flag after ops kubectl is rejected by the kubectl command and the root's handler never sees it. hintPlacement now walks the mounted tree after mountOps and sets it on every command that has none, covering both transports.

I only caught that by rebuilding aosguard and running the reported invocation. My probe had exercised a root-level flag, which is the one placement this failure is not.

Where it stops

The rebuilt binary still prints the bare message. The reason is the layering:

specgen version v0.180.0 (umbra ref v0.180.0)

codegen.Render lives in umbra, but the installed specgen binary embeds its own compiled copy of the template. --umbra-ref pins the module the generated code imports, not the template that generates it. String-searching the rebuilt aosguard confirms both halves: umbra@v0.184.0 is present, and none of the new handler text is.

So this issue closes when specgen is released past v0.184.0 and installed, then aosguard is rebuilt. Nothing further is needed in umbra.

The verification that should be run when it is

The bare assertion is not enough, because a handler that decorates everything would also pass it. Three cases:

ops kubectl --context ser8 get pods   -> bare error PLUS the placement sentence
ops forgejo repo delete torvalds linux -> the scope-gate refusal, UNCHANGED
ops forgejo user-repo list coilysiren  -> still resolves

The second is the one that matters. A genuine capability denial must not acquire the hint, or the fix trades one indistinguishable message for another.

What this cost, as evidence for the milestone

Two seats read a parse error as an architectural claim fifteen days apart, and tonight I shipped a fix for it that did not fire and would have read as done from the commit alone. Same shape as the thing being fixed, three layers up.

Built, landed upstream, and **not yet in effect**. Recording precisely where it stops, because the change looks shipped and is not. ## What landed Two commits in umbra, released as **v0.184.0**: * `0530f5b` adds `placementHint`, an `OnUsageError` that appends what the message was missing, and only for this error class: > ... . This is argument placement, not a denied capability: a flag for the wrapped tool goes after the verb, not before it * `2f51a6f` fixes the first attempt. The handler was on the root command alone and **never fired**. urfave raises the parse error on the command that owns the flag, so a flag after `ops kubectl` is rejected by the kubectl command and the root's handler never sees it. `hintPlacement` now walks the mounted tree after `mountOps` and sets it on every command that has none, covering both transports. I only caught that by rebuilding aosguard and running the reported invocation. My probe had exercised a root-level flag, which is the one placement this failure is not. ## Where it stops The rebuilt binary still prints the bare message. The reason is the layering: ``` specgen version v0.180.0 (umbra ref v0.180.0) ``` `codegen.Render` lives in umbra, but the **installed `specgen` binary embeds its own compiled copy of the template**. `--umbra-ref` pins the module the generated code imports, not the template that generates it. String-searching the rebuilt aosguard confirms both halves: `umbra@v0.184.0` is present, and none of the new handler text is. So this issue closes when specgen is released past v0.184.0 and installed, then aosguard is rebuilt. Nothing further is needed in umbra. ## The verification that should be run when it is The bare assertion is not enough, because a handler that decorates everything would also pass it. Three cases: ``` ops kubectl --context ser8 get pods -> bare error PLUS the placement sentence ops forgejo repo delete torvalds linux -> the scope-gate refusal, UNCHANGED ops forgejo user-repo list coilysiren -> still resolves ``` The second is the one that matters. A genuine capability denial must not acquire the hint, or the fix trades one indistinguishable message for another. ## What this cost, as evidence for the milestone Two seats read a parse error as an architectural claim fifteen days apart, and tonight I shipped a fix for it that did not fire and would have read as done from the commit alone. Same shape as the thing being fixed, three layers up.
Author
Owner

Verified fixed in the shipped binary, not just in the source.

aosguard version aos-v0.271.0, downloaded from the release rather than built locally:

$ aosguard --labels=x ops forgejo issue get coilyco-flight-deck agentic-os 1
aosguard: flag provided but not defined: -labels. This is argument placement,
not a denied capability: a flag for the wrapped tool goes after the verb, not
before it

$ aosguard ops forgejo issue --labels=x get ...          # same hint
$ aosguard ops forgejo issue get --nope=1 ...            # same hint

All three positions, including the mid-path one. That middle case matters: my first fix set OnUsageError on the root command only, and urfave raises the error on the command that owns the flag, so the hint never fired for anything below the root. hintPlacement now walks the whole tree.

The second thing that made this look landed when it was not: codegen.Render lives in umbra, but the installed specgen binary embeds its own compiled copy of the template. --umbra-ref pins what generated code imports, never the generator itself. So bumping umbra changed nothing until ARG SPECGEN_VERSION in docker/dev-base/full/Dockerfile went 0.175.0 to 0.184.0.

That pin landed in 52768a7c (#1388) and aos-v0.269.0, 0.270.0 and 0.271.0 all carry it.

One note for anyone reading this on an older machine: a locally installed aosguard predating aos-v0.269.0 still shows the bare error. Mine reports aos-v0.266.0. Upgrade rather than re-diagnose.

Verified fixed in the shipped binary, not just in the source. `aosguard version aos-v0.271.0`, downloaded from the release rather than built locally: ``` $ aosguard --labels=x ops forgejo issue get coilyco-flight-deck agentic-os 1 aosguard: flag provided but not defined: -labels. This is argument placement, not a denied capability: a flag for the wrapped tool goes after the verb, not before it $ aosguard ops forgejo issue --labels=x get ... # same hint $ aosguard ops forgejo issue get --nope=1 ... # same hint ``` All three positions, including the mid-path one. That middle case matters: my first fix set `OnUsageError` on the root command only, and urfave raises the error on the command that owns the flag, so the hint never fired for anything below the root. `hintPlacement` now walks the whole tree. The second thing that made this look landed when it was not: `codegen.Render` lives in umbra, but the installed `specgen` binary embeds its own compiled copy of the template. `--umbra-ref` pins what generated code *imports*, never the generator itself. So bumping umbra changed nothing until `ARG SPECGEN_VERSION` in `docker/dev-base/full/Dockerfile` went 0.175.0 to 0.184.0. That pin landed in `52768a7c` (#1388) and `aos-v0.269.0`, `0.270.0` and `0.271.0` all carry it. One note for anyone reading this on an older machine: a locally installed `aosguard` predating aos-v0.269.0 still shows the bare error. Mine reports aos-v0.266.0. Upgrade rather than re-diagnose.
Sign in to join this conversation.
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/agentic-os#1366
No description provided.