Two exec spellings that failed quietly now fail at load
Follow-up to
exec-authority-partitioned,
which fixed the internal exec type. The surface encoding kept two warts, both
of the same kind: a plausible spelling that the decoder accepted and then gave
the author something other than what it looks like. Neither is a lattice
question — the value vocabulary (two-valued for directories, three-valued for
literal commands, argv[0] depth only) stands.
A path-shaped literal key that is a directory is an error
The trailing slash is the whole of what separates a subpath key from a literal
one. So exec: ['/usr/bin': 'allow'] decoded to a literal grant on a binary
at the path /usr/bin — a file that cannot exist. It matched nothing, and
deny-by-default then refused every command the author meant to admit, surfacing
much later as a bare “denied by active grant” with no hint that a slash was the
cause. Failing closed, but unreadably.
freeze_exec_map now stats a path-shaped literal after freezing it and errors
if it is a directory, hinting '/usr/bin/'. The freeze pass already consults
the environment — path: reads $PATH, sigils resolve against the FreezeCtx
— so asking the filesystem there is in keeping with what freezing already is.
The alternative was an explicit dir: sigil alongside path:/xdg:/cwd:,
making the kind spelled rather than shape-inferred. Rejected as the larger
change to a widely understood convention: the slash reads correctly, it just
needed to stop being silent when omitted.
An empty subcommand list is an error, not a third spelling of 'allow'
The decoder mapped [] to ExecPolicy::Allow. But
Subcommands(s₁).meet(Subcommands(s₂)) is set intersection and legitimately
produces Subcommands(∅), which check_exec_args enforces as deny every
invocation. The same object meant ⊥ inside the lattice and ⊤ at the surface.
It was also anti-monotone at the boundary: deleting entries from a subcommand
list shrinks authority right up to the last deletion, which jumps to full
authority — the same inversion the 260602 dir-meet fix closed elsewhere. Given
the decoder’s otherwise strict brand (rejects true, rejects capitalised
Allow, errors on unknown keys), the empty list is now an error naming both
real spellings.
decode_exec_grant_empty_list_means_allow pinned the old behaviour and is
replaced by its inverse; the []-as-allow spelling is gone from SPEC.md,
TUTORIAL.md, both READMEs, and every test and demo script that used it.