fix(users): the root-door resolver matches whole fields, not substrings

Caught in review. root_door_of matched unanchored substrings, so any value
that EXTENDS a real one resolved as that value: `root-door=closedish` read as
`closed` and PASSED close-root's gate -- the one arm in this repo that
authorizes an irreversible act -- and `class=humanoid` did the same through
the compat arm. Both contradicted the function's own header, which promises a
value outside the set resolves empty and fails closed.

Only reachable by hand-editing a marker, so it was never a live incident. It
gets fixed anyway because this is the single function every consumer trusts --
close-root's gate, apply's root-SSH note, and bootstrap-tenant's machine
guard all ask it -- and a resolver that is nearly right about a root door is
the wrong kind of nearly.

The marker is one line of space-separated key=value fields (bootstrap writes
it with a single printf), so padding both ends and matching on field
boundaries is exact rather than heuristic. Whitespace is normalised first so a
hand-edit using tabs still reads correctly -- anchoring must not trade one
silent misread for another.

BOTH vocabularies are anchored. Fixing only the current spelling would have
left the hole open on every box bootstrapped before #77, which is precisely
the population the compat arm exists to serve.

Tests pin the resolver and the end-to-end refusal, since the resolver
returning "" is only safe because consumers treat it as one. Reverting the
anchoring turns the suite red (447/4); restoring it returns 451/0. The
original compat proof still holds: removing the class= arm gives 441/10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-20 11:18:37 +00:00
parent b1c1357f2b
commit 4bbf1babe0
3 changed files with 60 additions and 7 deletions

View file

@ -67,6 +67,21 @@ on the way to cutting its first release, and this file starts there.
told to re-run bootstrap — never a door welded shut on a machine whose only told to re-run bootstrap — never a door welded shut on a machine whose only
entrance it was. entrance it was.
The resolver matches **whole fields, not substrings** — the marker is one
line of space-separated `key=value` pairs, so it pads both ends and matches
on field boundaries. Review caught the first cut doing unanchored matching,
which resolved any value that *extended* a real one: `root-door=closedish`
read as `closed` and passed close-root's gate — the single arm that
authorizes an irreversible act — and `class=humanoid` did the same through
the compat arm, both contradicting the resolver's own promise that a value
outside the set resolves empty and fails closed. Only reachable by hand-
editing a marker, so never a live incident, but this is the one function
every consumer trusts and it owes them exactness rather than nearly. Both
vocabularies are anchored: fixing only the current spelling would have left
every pre-#77 box carrying the hole. Whitespace is normalised first, so a
hand-edit using tabs reads the same rather than trading one silent misread
for another.
**New markers are written in the new vocabulary only.** Writing both would **New markers are written in the new vocabulary only.** Writing both would
keep an old rig reading a new marker, but it would entrench the retired keep an old rig reading a new marker, but it would entrench the retired
spelling on every box rig ever converges and make the disagreement case spelling on every box rig ever converges and make the disagreement case

View file

@ -158,14 +158,27 @@ read_role_marker() {
# unchanged from before: a marker that names no door policy cannot authorize # unchanged from before: a marker that names no door policy cannot authorize
# shutting a door. # shutting a door.
root_door_of() { root_door_of() {
local marker="$1" new="" old="" local marker="$1" new="" old="" padded
case "$marker" in # FIELD-ANCHORED, not substring. The marker is one line of space-separated
*root-door=closed*) new=closed ;; # `key=value` fields (bootstrap writes it with a single printf), so padding
*root-door=open*) new=open ;; # both ends and matching whole fields is exact. Unanchored patterns matched
# any value that EXTENDS a real one: `root-door=closedish` resolved as
# `closed` and passed close-root's gate — the one arm that authorizes an
# irreversible act — and `class=humanoid` did the same through the compat
# arm. That contradicted this function's own promise above, that a value
# outside the set resolves empty and fails closed. Only reachable by hand
# editing, but this is the function every consumer trusts, so it owes them
# exactness rather than "close enough" (found in review on #77).
# Whitespace is normalised first so a hand-edit using tabs or double spaces
# is read the same way rather than silently failing to match.
padded=" ${marker//[[:space:]]/ } "
case "$padded" in
*" root-door=closed "*) new=closed ;;
*" root-door=open "*) new=open ;;
esac esac
case "$marker" in case "$padded" in
*class=human*) old=closed ;; *" class=human "*) old=closed ;;
*class=server*) old=open ;; *" class=server "*) old=open ;;
esac esac
if [ -n "$new" ] && [ -n "$old" ] && [ "$new" != "$old" ]; then if [ -n "$new" ] && [ -n "$old" ] && [ "$new" != "$old" ]; then
printf 'conflict'; return 0 printf 'conflict'; return 0

View file

@ -1604,6 +1604,31 @@ check "root_door_of: a tenant marker names no door at all" 0 "[]" \
door_of 'role=claude-box tenant=yes host=no' door_of 'role=claude-box tenant=yes host=no'
check "root_door_of: disagreement is a conflict, not a coin flip" 0 "[conflict]" \ check "root_door_of: disagreement is a conflict, not a coin flip" 0 "[conflict]" \
door_of 'role=custom root-door=open class=human host=no join=login' door_of 'role=custom root-door=open class=human host=no join=login'
# FIELD-ANCHORED, not substring (found in review on #77). A value that EXTENDS
# a real one must resolve EMPTY and fail closed, exactly as the function's
# header promises — before anchoring, `closedish` read as `closed` and PERMITTED
# close-root, the one arm that authorizes an irreversible act. Both vocabularies
# are checked: the compat arm had the identical hole, and a fix that anchored
# only the current spelling would leave every pre-#77 box exposed to it.
check "root_door_of: a value EXTENDING the current spelling resolves empty" 0 "[]" \
door_of 'role=x root-door=closedish host=no'
check "root_door_of: a value extending the pre-#77 spelling resolves empty too" 0 "[]" \
door_of 'role=x class=humanoid host=no'
check "root_door_of: a value PREFIXED by junk does not match either" 0 "[]" \
door_of 'role=x notroot-door=closed host=no'
# ...and the gate itself must refuse on those, not merely resolve empty: the
# resolver returning "" is only safe because every consumer treats it as a
# refusal, so the end-to-end behaviour is what gets pinned.
DOOR_FIX="$(mktemp -d)"
printf 'role=x root-door=closedish host=no\n' > "$DOOR_FIX/bogus"
# shellcheck disable=SC2016 # $1/$2 are the inner shell's positionals, not ours
check "close-root: refuses a marker whose door value merely LOOKS closed" 1 "names no root-door policy" \
bash -c '. "$1/commands/lib/users-config.sh"; assert_marker_closes_root "$2"' _ "$ROOT" "$DOOR_FIX/bogus"
# Whitespace normalisation: a hand-edit using tabs is still a real marker and
# must read the same, or anchoring would trade one silent misread for another.
check "root_door_of: tab-separated fields read the same as space-separated" 0 "[closed]" \
door_of "$(printf 'role=x\troot-door=closed\thost=no')"
rm -rf "$DOOR_FIX"
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
check "users close-root: refuses non-root" 1 "must run as root" "$ROOT/commands/users-close-root.sh" check "users close-root: refuses non-root" 1 "must run as root" "$ROOT/commands/users-close-root.sh"
else else