diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9577e..47dfaca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,17 @@ which records not just what changed but what each drill run proved. records **one hop** — a clone of a clone names its parent and forgets its grandparent, because the alternative is an unbounded chain in a config value and the parent is the box an operator can go look at. + One key sits in **neither** column and is therefore *cleared*: + `user.box.mode.asked`. It records whether a container was asked for or + fallen back into, which makes it a mint-*event* fact — and the asker was + the **source's** operator. A clone refuses `--vm`/`--container` outright, + so nobody was asked anything about this instance, and there is no true + value to re-stamp it with; inheriting it made `box info` print + `MODE vm (asked: auto)` on a clone, describing a demand never made of it. + The clear lands with the re-stamp, before the start, and the read side + needs no special case: the `MODE` line is gated on `asked`, so absence + renders as silence while `TYPE` still reports VM or CT off the preserved + instance type. - **`box info` grew a provenance block** (#103) — it printed `NAME / STATE / TYPE / IPV4`, exposures and snapshots, and surfaced *none* of the `user.box.*` keys, including the two that already existed. A stamp diff --git a/bin/box b/bin/box index 40b54b3..f36ca3f 100755 --- a/bin/box +++ b/bin/box @@ -1079,6 +1079,17 @@ cmd_new() { # that rig role — reading them off the source is the whole point of a clone. # ('incus copy' preserves the instance type too, so mode stays true as well.) # + # 'mode.asked' is the one key that sits in NEITHER column, and so it is + # CLEARED rather than re-stamped or inherited. It is a mint-event fact — + # only the mint knew whether a container was asked for or fallen back into + # for want of /dev/kvm — and the asker was the SOURCE's operator. A clone + # refuses --vm/--container outright (nobody was asked anything here), so an + # inherited 'asked' makes 'box info' print a demand that was never made of + # this instance. There is no true value to re-stamp it with: the honest + # answer is absence, and absence is already how the whole block renders + # what it does not know — the MODE line simply does not print, while TYPE + # above still says VM or CT off the preserved instance type. + # # origin.from records ONE hop. A clone of a clone names its parent and # forgets its grandparent: the alternative is an unbounded chain in a config # value, and the parent is the box an operator can actually go look at. @@ -1088,6 +1099,11 @@ cmd_new() { user.box.created="$(mint_time)" \ user.box.origin=clone \ user.box.origin.from="$srcref" + # Cleared, not set-to-empty: an empty value is still a key, and a reader + # that greps the config would find it. Tolerated failure because the source + # may predate the stamp and never have carried the key at all — a clone + # must not die over a key that was already absent. + incus config unset "$instance" user.box.mode.asked >/dev/null 2>&1 || true incus start "$instance" wait_agent "$instance" reset_identity "$instance" diff --git a/test/cli.sh b/test/cli.sh index f1d4657..c9e2abb 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -1256,6 +1256,17 @@ check "clone: does NOT re-stamp the image — the disk really came from it (#103 restamp_has "$CLONELOG" 'user\.box\.image' check "clone: does NOT re-stamp the role — the source's rig converged it (#103)" 1 "" \ restamp_has "$CLONELOG" 'user\.box\.role' +# The third column, and the one review found: 'mode.asked' is neither lineage +# nor re-stampable. It is a mint-event fact whose asker was the SOURCE's +# operator, and a clone refuses --vm/--container, so nobody was asked anything +# here. It is CLEARED — there is no true value to give it. +check "clone: clears the inherited mode.asked — nobody asked THIS box (#103)" 0 "" \ + grep -qF 'config unset w2 user.box.mode.asked' "$CLONELOG" +check "clone: ...and does not re-stamp it with a fabricated answer (#103)" 1 "" \ + restamp_has "$CLONELOG" 'user\.box\.mode\.asked' +# Cleared, never set-to-empty: an empty value is still a key on the instance. +check "clone: clears it rather than setting it empty (#103)" 1 "" \ + grep -qE 'config set .*user\.box\.mode\.asked=($|[[:space:]])' "$CLONELOG" # Order: the re-stamp lands on the copied instance BEFORE it is started, so a # clone is never observable wearing its source's provenance. Fail-closed — an # absent line makes the arithmetic fail, not pass. @@ -1267,6 +1278,17 @@ restamp_precedes_start() { } check "clone: the re-stamp precedes the start (#103)" 0 "" \ restamp_precedes_start "$CLONELOG" +# The clear rides the same rule for the same reason: a clone must never be +# observable — not for one moment, not to 'box info' — wearing an 'asked' its +# operator never gave. Fail-closed the same way. +clear_precedes_start() { + local unset_ln start + unset_ln="$(grep -n 'config unset .* user.box.mode.asked' "$1" | head -1 | cut -d: -f1)" + start="$(grep -n '^incus start ' "$1" | head -1 | cut -d: -f1)" + [ -n "$unset_ln" ] && [ -n "$start" ] && [ "$unset_ln" -lt "$start" ] +} +check "clone: the mode.asked clear precedes the start too (#103)" 0 "" \ + clear_precedes_start "$CLONELOG" # --- the read half: 'box info' surfaces it --------------------------------- # A stamp nothing can read is not done. cmd_info printed NAME/STATE/TYPE/IPV4 @@ -1304,13 +1326,25 @@ check "info: surfaces the origin (#103)" 0 "ORIGIN mint" infobox "$STAMPED" # Still the box it always was: the new block is additive, above the snapshots. check "info: still prints the state block it always did" 0 "IPV4 10.1.2.3" infobox "$STAMPED" -# A clone reads back as a clone, naming its source. +# A clone reads back as a clone, naming its source. Modelled on what the +# --from branch actually leaves behind: origin re-stamped, mode.asked cleared. CLONECFG="$MWORK/clone.cfg" -{ grep -v '^user.box.origin ' "$STAMPED" +{ grep -v -e '^user.box.origin ' -e '^user.box.mode.asked ' "$STAMPED" echo 'user.box.origin clone' echo 'user.box.origin.from work/authed'; } > "$CLONECFG" check "info: a clone says so, and names the box it came from (#103)" \ 0 "ORIGIN clone of work/authed" infobox "$CLONECFG" +# ...and stays silent about a demand nobody made of it. The MODE line is gated +# on 'asked' precisely so absence renders as silence rather than as a guess; +# TYPE above still reports VM off the instance type, so nothing is lost. +info_has_mode() { infobox "$1" | grep -q '^MODE'; } +check "info: a clone prints no MODE line — nobody asked IT anything (#103)" 1 "" \ + info_has_mode "$CLONECFG" +check "info: ...while TYPE still reports what it actually is (#103)" \ + 0 "TYPE VM" infobox "$CLONECFG" +# The mint keeps its MODE line — there, the operator really was asked. +check "info: a MINT still surfaces what was asked for (#103)" \ + 0 "MODE vm (asked: auto)" infobox "$STAMPED" # --- legacy boxes: the promise that they keep working under every verb ------ # A box carrying the boundary tag and NOTHING else — every box minted before