options: a named membership set in the guardfile, so a large literal allowlist is not a list of globs #307
Labels
No labels
burndown-2026-06
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/umbra#307
Loading…
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?
Kai's proposal, 2026-08-19, recorded by Saiya (exec seat). Sibling in shape to #306: both extend the frozen inline grammar because the thing wanted cannot be expressed by composing what is there.
The motivating case
A guardfile wrapping Wikipedia's REST API, restricted to a curated collection: the 999 articles on
Wikipedia:Vital articles/Level 3. Measured today, that list is 999 titles, 14,133 bytes as a JSON array, and every title is a literal string.Kai's position, which shapes this whole issue: a 999-line guardfile is fine here, and preferred. The list is the collection manifest. In the guardfile it is reviewable, diffable, and greppable, and a change to what the agent may reach is a change to the guard rather than to a data file somewhere else. This issue therefore does not propose sourcing the set from a file or a provider. The literals belong in the KDL.
What exists
restrict <param> matches "<glob>"..., parsed athttp/guardfile/guardfile.go:792and dispatched athttp/opcore/inline.go:88. Wrap-level, fail-closed before any wire call, enforced on the action path, inherited deduped by param with the child winning.It already does the enforcement half correctly, and I want to be clear that this issue is not filed because
restrictis broken. Arestrict title matchesblock with 999 entries would work today, at zero context cost, which is more than I initially credited it with.What it does not do, in descending order of how much it matters
1. It is a glob matcher being asked to test membership.
Restrictionis documented atguardfile.go:100as taking globs, and the parse error atguardfile.go:797says so. Every one of those 999 entries would be compiled and evaluated as a pattern.I checked whether this actually bites for this list, and it does not: zero of the 999 titles contain
*,?,[,], or a backslash. Twenty contain parentheses, which are not glob metacharacters. So for the motivating case this is a correctness non-issue and I am not going to pretend otherwise.The argument is about the general case and about direction of failure. A metacharacter appearing in a literal makes a glob match more than the author wrote, which widens an allowlist. In a framework whose entire discipline is failing closed, the one construct that silently fails open on a typo is worth replacing with an exact-set test. It is also the kind of bug that is invisible in review, because the guardfile line looks exactly like the 998 correct ones next to it.
2. It is not nameable, so it cannot be shared or stated.
restrictbinds a list to one{param}name. A wrap with two grants taking the same collection under different path-parameter names needs two copies of 999 lines, and nothing in the file says they are meant to be the same set. A named set is declared once and referenced.3. Nothing can say what the set is.
There is no way for
describeoutput,--help, or a tool schema to carry "restricted to the 999 Wikipedia vital articles". The choice today is silence or the whole list. For an MCP consumer that is the difference between an agent that knows it holds a bounded collection and one that discovers the boundary by being refused 999 times.4. There is nowhere to hang schema emission.
Confirmed by sweep: umbra has no enum or schema-value-set concept anywhere. The only two occurrences of "enum" in
docs/are the English word "enumerate", inspecverb-policy.md:27andspecverb-resolution.md:25. So there is currently no answer to "which values may this field take" beyond type and bounds.Rough shape
Naming to be settled in review. The reference syntax below is the part I am least sure about.
Properties worth having, whatever the spelling:
restrict ... innaming an undeclared set, and a declared set nothing references are all build errors. The last one matters: a 999-line block nobody uses is exactly the kind of thing that rots silently.restrict <param> matches "<glob>"keeps working untouched. This adds a second, stricter mode rather than reinterpreting the first, so no existing guardfile changes meaning.The thing this does NOT solve, stated plainly
Discovery. A consumer still has no cheap way to learn what is in the set.
The obvious move is emitting the members as JSON Schema
enumon the field. For a set this size that is the wrong default and probably the wrong option entirely. 14,133 bytes in a tool schema is paid on every turn carrying the roster, against measurements incoilyco-gaming/sirens-echo#932of a 116 KB fixed prefix and#1002of that prefix being paid roughly six times per reply. Emitting this set would be a measurable regression on a lane already being worked for exactly that.So if schema emission is built at all it should be opt-in and default off, and the
describestring is the real discovery mechanism: one sentence naming what the collection is, at roughly 60 bytes rather than 14,133. That is enough for an agent to describe its own boundary honestly, which is the capability-honesty requirement incoilyco-gaming/sirens-echo#200, and it is not enough for the agent to enumerate the shelf. Enumerating the shelf is a consumer-side problem and does not belong in this construct.I would rather this issue land as enforcement plus a description and be honest that discovery is unsolved, than grow a schema-emission feature whose default costs every consumer context they did not ask for.
Open questions I am not deciding
applyNodeatinline.go:75owns the wrap body and its frozen set isbase-url | auth | header | restrict | can | proxy. A top-leveloptionssibling towrapreads better, butinheritresolves textually by splicing the wrap body perspecverb-policy.md:23, so a top-level node may not survive inheritance. That interaction should decide the placement rather than aesthetics.restrict <param> in <name>reuses the existing enforcement path.field "title" options=<name>would extend it to typed query fields, whichrestrictcannot reach at all today since it is bound to{param}path templates. The second is more useful and a larger change.describeauto-reports the count. "999 members" is derivable and useful, and it is also a number that goes stale in prose the moment someone adds a member.Consumer waiting on it
coilyco-bridge/deploy#720carries an epic that wants exactly this shape, and one child of it is an encyclopedia server whose whole design turns on a bounded curated collection. That work is not blocked, sincerestrict ... matcheswould carry it, but it would be authored twice if this lands afterward.Complete when
describeand--help.restrict ... matchesguardfiles are byte-for-byte unaffected.Refs: #306 for the parallel frozen-grammar extension,
coilyco-bridge/deploy#720.