feat(specverb): let an action input constrain its value, not just demand one #323

Merged
coilyco-ops merged 1 commit from aos/1105-input-matches into main 2026-08-27 03:57:26 +00:00
Owner

Closes #322. Unblocks coilyco-flight-deck/agentic-os#1105.

What was missing

#319 cleared presence: an action input can carry an array, and required is enforced while inputs bind, before the request is assembled. What it cannot do is say anything about the shape of the value, so a shadow can demand a label set and not demand a well-formed one.

agentic-os#1105 needs exactly that, and is explicit that a bare rejection does not count:

The refusal names which of the two is missing.

matches on an input

input labels {
    flag
    required
    array
    matches "priority/*" message="no priority label: pass --labels priority/P2 (P0-P4)"
    matches "autonomy/*" message="no autonomy label: pass --labels autonomy/headless"
}

On an array input each constraint demands at least one matching element. Constraints are checked independently, which is the property that lets the error name the missing axis rather than the whole set: four labels carrying no priority/* fail on the priority constraint alone. On a scalar input the bound value itself must match.

Checked where required is checked, so a refusal ends the run before any request exists. The tests prove that rather than assert it: both refusal cases run with a transport that fails the test if anything reaches the wire.

Why this spelling

matches is already the DSL's word for a glob guard in three places - restrict <param> matches <glob...> at wrap scope, when first input matches ... on a fetch, and when/deny-when <sel> matches <glob...> in execverb. This reuses the word and the matcher rather than adding a second vocabulary for the same idea.

matchesAnyGlob is exported as opcore.MatchesAnyGlob so the input guard and the wrap restriction cannot drift apart, following #319's precedent of moving shared encoding into opcore rather than letting it exist twice.

Checking on names, not ids

Worth recording, because it is why the guard is useful at all. agentic-os#1105 rules out an id allowlist: label names are consistent across the estate and ids are not, so an allowlist is one stale table per org. Resolving names to ids in a lookup step is separately blocked, since stepflow.SliceRefs documents that a step-output reference is never a list. Constraining the tokens the caller already passed needs neither.

Fail-closed edges

An absent glob, an empty glob, a second glob, and any property other than message each fail at parse rather than silently constraining nothing.

Verification

make test (34 packages, no failures), make vet, make lint (0 issues), make tidy, and godoc-current.txt regenerated. Seven new tests: three on the composition guard end to end, four on the parser's fail-closed edges.

pre-commit run --all-files passes every hook except code-comments, which reports 18 violations on main unchanged - this branch adds none. Confirmed by diffing the hook's output against the base commit.

No API break

Additive. Input gains a field and opcore.matchesAnyGlob is exported under the same behaviour, so no consumer signature changes.

Closes #322. Unblocks `coilyco-flight-deck/agentic-os#1105`. ## What was missing #319 cleared presence: an action input can carry an `array`, and `required` is enforced while inputs bind, before the request is assembled. What it cannot do is say anything about the **shape** of the value, so a shadow can demand a label set and not demand a well-formed one. agentic-os#1105 needs exactly that, and is explicit that a bare rejection does not count: > The refusal names **which** of the two is missing. ## `matches` on an input ```kdl input labels { flag required array matches "priority/*" message="no priority label: pass --labels priority/P2 (P0-P4)" matches "autonomy/*" message="no autonomy label: pass --labels autonomy/headless" } ``` On an `array` input each constraint demands **at least one** matching element. Constraints are checked independently, which is the property that lets the error name the missing axis rather than the whole set: four labels carrying no `priority/*` fail on the priority constraint alone. On a scalar input the bound value itself must match. Checked where `required` is checked, so a refusal ends the run before any request exists. The tests prove that rather than assert it: both refusal cases run with a transport that fails the test if anything reaches the wire. ## Why this spelling `matches` is already the DSL's word for a glob guard in three places - `restrict <param> matches <glob...>` at wrap scope, `when first input matches ...` on a `fetch`, and `when`/`deny-when <sel> matches <glob...>` in execverb. This reuses the word and the matcher rather than adding a second vocabulary for the same idea. `matchesAnyGlob` is exported as `opcore.MatchesAnyGlob` so the input guard and the wrap restriction cannot drift apart, following #319's precedent of moving shared encoding into opcore rather than letting it exist twice. ## Checking on names, not ids Worth recording, because it is why the guard is useful at all. agentic-os#1105 rules out an id allowlist: label names are consistent across the estate and ids are not, so an allowlist is one stale table per org. Resolving names to ids in a lookup step is separately blocked, since `stepflow.SliceRefs` documents that a step-output reference is never a list. Constraining the tokens the caller already passed needs neither. ## Fail-closed edges An absent glob, an empty glob, a second glob, and any property other than `message` each fail at parse rather than silently constraining nothing. ## Verification `make test` (34 packages, no failures), `make vet`, `make lint` (0 issues), `make tidy`, and `godoc-current.txt` regenerated. Seven new tests: three on the composition guard end to end, four on the parser's fail-closed edges. `pre-commit run --all-files` passes every hook except `code-comments`, which reports **18 violations on `main` unchanged** - this branch adds none. Confirmed by diffing the hook's output against the base commit. ## No API break Additive. `Input` gains a field and `opcore.matchesAnyGlob` is exported under the same behaviour, so no consumer signature changes.
feat(specverb): let an action input constrain its value, not just demand one
All checks were successful
ci / secrets (pull_request) Successful in 10s
ci / lint (pull_request) Successful in 43s
ci / test (pull_request) Successful in 59s
45f05a1133
Closes #322.

## What was missing

#319 cleared presence: an action input can carry an `array`, and `required` is
enforced while inputs bind, before the request is assembled. What it cannot do
is say anything about the *shape* of the value, so a shadow can demand a label
set and not demand a well-formed one.

agentic-os#1105 needs exactly that, and is explicit that a bare rejection does
not count:

> The refusal names **which** of the two is missing.

## `matches` on an input

```kdl
input labels {
    flag
    required
    array
    matches "priority/*" message="no priority label: pass --labels priority/P2 (P0-P4)"
    matches "autonomy/*" message="no autonomy label: pass --labels autonomy/headless"
}
```

On an `array` input each constraint demands **at least one** matching element.
Constraints are checked independently, which is the property that lets the error
name the missing axis rather than the whole set: four labels carrying no
`priority/*` fail on the priority constraint alone. On a scalar input the bound
value itself must match.

Checked where `required` is checked, so a refusal ends the run before any
request exists. The tests prove that rather than assert it: both refusal cases
run with a transport that fails the test if anything reaches the wire.

## Why this spelling

`matches` is already the DSL's word for a glob guard in three places - `restrict
<param> matches <glob...>` at wrap scope, `when first input matches ...` on a
`fetch`, and `when`/`deny-when <sel> matches <glob...>` in execverb. This reuses
the word and the matcher rather than adding a second vocabulary for the same
idea.

`matchesAnyGlob` is exported as `opcore.MatchesAnyGlob` so the input guard and
the wrap restriction cannot drift apart, following #319's precedent of moving
shared encoding into opcore rather than letting it exist twice.

## Checking on names, not ids

Worth recording, because it is why the guard is useful at all. agentic-os#1105
rules out an id allowlist: label names are consistent across the estate and ids
are not, so an allowlist is one stale table per org. Resolving names to ids in a
lookup step is separately blocked, since a step-output reference is never a
list. Constraining the tokens the caller already passed needs neither.

## Fail-closed edges

An absent glob, an empty glob, a second glob, and any property other than
`message` each fail at parse rather than silently constraining nothing.

## Verification

`make test` (34 packages, no failures), `make vet`, `make lint` (0 issues),
`make tidy`, and `godoc-current.txt` regenerated. Seven new tests: three on the
composition guard end to end, four on the parser's fail-closed edges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
coilyco-ops deleted branch aos/1105-input-matches 2026-08-27 03:57:27 +00:00
Sign in to join this conversation.
No reviewers
No milestone
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/umbra!323
No description provided.