Skip to content

Decision 0015: operator facing commands carry the Keel name - #3

Merged
marcos-mendez merged 3 commits into
mainfrom
docs/decision-0015-keel-named-commands
Sep 29, 2026
Merged

marcos-mendez merged 3 commits into
mainfrom
docs/decision-0015-keel-named-commands

Conversation

@marcos-mendez

@marcos-mendez marcos-mendez commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Cross-reference only, nothing is closed from here: Keel-Linux/tracker#12 is the org-wide issue, and the implementation lives in its own repository issue, Keel-Linux/keel-wordpress#7, with Keel-Linux/keel-wordpress#6 as its pull request. The handbook carries the policy and nothing else.

Merge this before Keel-Linux/keel-wordpress#6, which cites decision 0015 in its changelog, COVERAGE.md, tests/wrappers.bats and both script headers.

docs/decisions/0015-operator-command-names.md records the rule the maintainer decided on 2026-09-28: the Keel name is the real command, the turnkey-* name is kept as a symlink to it, and both keep working. It is a general policy, not a per-appliance choice, and the reason for keeping the old name is the same one that keeps /etc/turnkey_version in 0014 — compatibility with TurnKey appliances is a stated property of this project (0008).

Responding to the review

HIGH — the inventory contradicted the note's own scope rule. Correct, and it did so in the direction that makes a contributor under-apply the policy. The rule covers "a script installed by a package this project builds"; the table listed only the two keel-wordpress overlay files, the two in common, and the foreign packages going to tracker#13. So the note handled somebody else's packages and silently dropped the ones we build.

I verified the five myself against the dpkg file lists and against the forks' own trees, and added them:

Command Package Source
/usr/sbin/turnkey-init inithooks 2.3.6+keel4 Keel-Linux/inithooks, turnkey-init
/usr/sbin/turnkey-sudoadmin inithooks 2.3.6+keel4 Keel-Linux/inithooks, turnkey-sudoadmin
/usr/sbin/turnkey-install-security-updates inithooks 2.3.6+keel4 Keel-Linux/inithooks, turnkey-install-security-updates
/usr/lib/inithooks/bin/turnkey-init-fence inithooks 2.3.6+keel4 Keel-Linux/inithooks, bin/turnkey-init-fence
/usr/bin/turnkey-lexicon confconsole 2.2.3+keel2 Keel-Linux/confconsole, turnkey-lexicon

Also added, from LOW 1: turnkey-artisan and turnkey-composer, tracked 100755 files in common reached through laravel.mk and composer.mk; and the byte-identical second copy of turnkey-mysql-install-perf-info-schemas in unit-mariadb (same blob c8f185b2), which the old table attributed to common alone, so anybody following it would have renamed one of two copies. turnkey-make-ssl-cert from turnkey-ssl 3.1.1 is now listed too, explicitly out of scope, so the next reader does not have to derive that again.

The section is restructured: fifteen distinct names grouped by what puts the file on the machine — appliance overlay, common overlay, a package we build, a package somebody else builds — because that is what decides when each one can move. Eleven are ours. turnkey-init-fence is marked as not on PATH and not a command an operator types, with the observation that the same name is also a conffile, a systemd unit and two firstboot hooks, which is a packaging question the note does not settle. And the section now closes by saying that when the table and the rule disagree, the rule wins — a table is a measurement of one day and this one has been corrected three times.

I took the reviewer's framing on the +keel suffix: the rule reads on "a package this project builds", and turnkey-sysinfo 1.1.0, turnkey-version 1.1.3 and turnkey-ssl 3.1.1 carry no such suffix while inithooks and confconsole do. The "what a package owns does instead" section now opens by saying it is about packages we do not build, since getting that the wrong way round is precisely the mistake the first version made.

MEDIUM — the -d claim. Correct, and worse in a note than in code, because a note is applied without asking. Measured with coreutils 9.7 and written into the note as a table:

cp -TR   →  link -> real     symlink kept, hard link split (nlink 1)
cp -TdR  →  link -> real     symlink kept, hard link kept  (nlink 2)
cp -TLR  →  link             a second regular file — this one flattens

The property is the absence of -L, not the presence of -d. keel-wordpress#6 now has a test asserting all three rows, so the claim is checked rather than written down.

LOW 2 — the Decision section now states that the link is relative and why an absolute one breaks the build-time caller, and that the Keel name is the turnkey- prefix replaced by keel- and nothing else. LOW 3 — "must be covered", and it names who is owed those tests. LOW 4 — the reverse-dependency provenance says the container throughout. LOW 5 — merge order is stated at the top of both pull request descriptions.

Test plan

  • The number is 0015 and only 0015; 0014 is being written concurrently and is untouched here.
  • The note follows the form of 0001-0013: title line, Date, Status, ## Decision, the three-part justification of brief section 10, ## Consequences.
  • Every added row verified against dpkg -L/dpkg -S file lists, git ls-files across the organization's repositories, and the .mk files that pull each overlay.
  • Maintainer confirms the note matches the decision taken on 2026-09-28, and in particular the reviewer's reading that "a package this project builds" means a package carrying a +keel version.

This repository ships no package and has no package / changelog gate, so there is no changelog entry to add, and no checks are reported on the branch.

The Keel name becomes the real command and the turnkey-* name is kept as a
symlink to it, both working, as a general policy rather than a per-appliance
choice. Compatibility with TurnKey is a stated property of this project
(0008), so an operator who pasted a command out of TurnKey's documentation,
or who wrote a script against turnkey-wp last year, must not silently lose
it.

The note says which of tracker#12's commands the policy moves now and which
wait, and corrects two things in that table. turnkey-php and
turnkey-mysql-install-perf-info-schemas are called inherited there; they are
ordinary files in the common repository of this organization, reaching the
image through COMMON_OVERLAYS, so the policy applies to them in full and what
makes them wait is the rebuild of the whole chain, not ownership. And
turnkey-sysinfo ships turnkey-detect-virt as well, which the table missed.

A turnkey-* name owned by a Debian package is out of scope here. It is
settled by owning the package: the maintainer decided on 2026-09-28 to build
keel-version and keel-sysinfo as Keel's own source packages rather than wrap
or divert TurnKey's, which is tracker#13. The pair is a closed set, which is
what makes that possible: turnkey-sysinfo 1.1.0 has no reverse dependencies
and turnkey-version 1.1.3's only one is turnkey-sysinfo.

The note also records which symlink mechanism was verified and how. The link
is committed into the overlay, because fab-apply-overlay is cp -TdR and -d
keeps a link a link, and that was checked by making the copy and running the
copied command rather than by reading the flag.
@marcos-mendez

Copy link
Copy Markdown
Contributor Author

Review

I checked the note's factual claims against fab's source, this organization's repositories and the booted wordpress-demo container rather than taking them as read. The reverse-dependency argument, the turnkey-detect-virt correction and the common-ownership correction all hold. One finding is HIGH: the note's inventory contradicts the note's own scope rule, and it does so in the direction that makes a future contributor under-apply the policy.

Verified

  • Lines 46-51, the closed set: correct. On the container, apt-cache rdepends turnkey-sysinfo is empty both installed-only and archive-wide, and turnkey-version's only reverse dependency is turnkey-sysinfo (turnkey-sysinfo: Depends: python3:any, turnkey-netinfo, turnkey-version). Versions are 1.1.0 and 1.1.3 as stated.
  • Lines 53-60, six not five: correct. dpkg -L turnkey-sysinfo lists /usr/bin/turnkey-sysinfo, /usr/bin/turnkey-detect-virt, libsysinfo/{__init__,disk,memstats,virt}.py and /usr/share/turnkey-sysinfo/contrib/motd. The library and the motd fragment are real and tracker#13 does need both names.
  • Lines 78-89, the common correction: correct. overlays/php/usr/local/bin/turnkey-php and overlays/mysql/usr/local/bin/turnkey-mysql-install-perf-info-schemas are ordinary tracked 100755 files in Keel-Linux/common, reaching the image through COMMON_OVERLAYS. On the appliance both are NOT owned by any package, which settles "inherited" against tracker#12. The single caller inside common is real: overlays/mysql/usr/lib/confconsole/plugins.d/System_Settings/Mysql_perf_info.py:19-20 calls it by bare name.
  • Lines 34-37, the dpkg reasoning: correct as stated.
  • Lines 102-114: the keel-wordpress tests do what the note says they do. I ran them.

HIGH — the table omits five commands that the note's own scope rule pulls in, and they ship on every appliance

Lines 15-20 against the table at 66-72.

The rule is: the policy applies to "every operator facing command a repository of this organization writes and names itself: a file in an appliance overlay, a file in the common overlays, a script installed by a package this project builds". The carve-out is for "a command that only exists because some other project's package put it there".

Measured on the booted wordpress-demo container, the third clause covers five commands the table never mentions:

Command Package Version installed
/usr/sbin/turnkey-init inithooks 2.3.6+keel4
/usr/sbin/turnkey-sudoadmin inithooks 2.3.6+keel4
/usr/sbin/turnkey-install-security-updates inithooks 2.3.6+keel4
/usr/lib/inithooks/bin/turnkey-init-fence inithooks 2.3.6+keel4
/usr/bin/turnkey-lexicon confconsole 2.2.3+keel2

Both packages carry a +keel suffix — they are built from Keel-Linux/inithooks and Keel-Linux/confconsole and keel-wordpress's own Makefile:40-41 names them among "the project's own packages". turnkey-init is the most operator-facing command on the appliance; it is what an operator types to re-run first-boot configuration.

Meanwhile the row the table does devote to packages sends turnkey-sysinfo, turnkey-version and turnkey-detect-virt to tracker#13 — and those come from turnkey-sysinfo 1.1.0 and turnkey-version 1.1.3, neither carrying a +keel suffix, i.e. the packages that are genuinely somebody else's. So the note handles the foreign packages and silently drops the ones this project already builds, which is the opposite of what the rule at 15-20 says. (A sixth, /usr/bin/turnkey-make-ssl-cert from turnkey-ssl 3.1.1, is also unmentioned; that one is correctly out of scope under the carve-out, but the note never says so either.)

The scenario this bites in is the one the note is written to prevent. A contributor picks up tracker#12, reads 0015 as the settled scope, sees turnkey-init is not in the table and concludes it is inherited surface. Nothing corrects them: there is no gate for a name, the note says at 153-156 that only grep -rn finds these, and the table has already been through two rounds of correction, which makes it read as authoritative.

The fix is small — either add the rows, or add one sentence saying the table covers the commands reachable from tracker#12's inventory and that packaged commands from inithooks and confconsole are unenumerated and still in scope.

MEDIUM — the -d claim is the wrong half of the flag, in the document a contributor will copy from

Lines 95-97: "-d is --no-dereference --preserve=links, so a symlink in an overlay arrives in the image as a symlink."

The conclusion is right and I verified it end to end. The attribution is not. In GNU cp, -P/--no-dereference is already the default under -R; only -L dereferences. Measured:

cp -TR  →  link -> real     (symlink preserved; hard link split, nlink 1)
cp -TdR →  link -> real     (symlink preserved; hard link kept, nlink 2)
cp -TLR →  link             (regular file — this is the one that flattens)

What -d contributes to fab-apply-overlay is --preserve=links, i.e. hard links. Symlink survival hinges on nobody passing -L.

This matters more here than in the code, because a decision note is applied without asking. As written it teaches that any copy step lacking -d will flatten links, which will get -d added where it does nothing, or a perfectly sound mechanism rejected for not having it. And the verification claimed at 102-109 cannot catch the difference: I dropped -d from tests/wrappers.bats's own cp and no test failed.

Suggested: "-R copies a symlink as a symlink unless -L is given, and -d additionally preserves hard links, so a symlink in an overlay arrives in the image as a symlink."


LOW 1 — two more common overlay commands, and a duplicate, unlisted

common/overlays/artisan/usr/local/bin/turnkey-artisan and common/overlays/composer/usr/local/bin/turnkey-composer are tracked 100755 files in common, reached through laravel.mk:3 and composer.mk:1. turnkey-artisan is the same runuser-based shape as turnkey-wp. Neither word appears anywhere in the note's 165 lines. They ship on no Keel appliance today, because nothing includes those mk files — which is exactly why they should be named rather than left to be rediscovered when something does.

Separately, unit-mariadb/overlay/usr/local/bin/turnkey-mysql-install-perf-info-schemas is a byte-identical second copy (same blob) of the common one. The table attributes that command to common alone, so a contributor following the note renames one of two copies and leaves the appliances built from the unit on the old name.

LOW 2 — the policy does not say the link must be relative, and the natural choice is wrong

The three bullets at 11-13 are otherwise applicable without asking. They do not say whether turnkey-x points at keel-x or at /usr/local/bin/keel-x. keel-wordpress chose relative and asserts it (readlink = keel-wp), which is the right answer: an absolute link does not resolve inside fab-chroot, so a build-time caller like conf.d/main breaks. I confirmed the failure mode — making the link absolute breaks three of that repository's tests. Absolute is the more natural thing to type, so one clause in the Decision section would save the next person the build.

Related and smaller: the note never states how the Keel name is derived. The table implies a mechanical turnkey- → keel- substitution; saying so removes the only other open question.

LOW 3 — a requirement written in the present tense reads as a description

Lines 157-159: "The compatibility names are covered by tests that run them, in every repository that has a pair." Today that is true of keel-wordpress and of nothing else — common has four such commands and no such test. As a Consequences bullet it is a rule; "must be covered" makes it one.

LOW 4 — where a measurement came from

Line 47 says the reverse-dependency figures were "measured on the wordpress-demo container", line 54 says the dpkg file list is "on the build host". Both numbers are right; I reproduced them on the container. The note is careful about provenance everywhere else, so the mismatch is worth a word.

LOW 5 — ordering against keel-wordpress#6

Keel-Linux/keel-wordpress#6 cites "decision 0015 of the handbook" in changelog, COVERAGE.md, tests/wrappers.bats and in both scripts' header comments. 0015 is not on main here yet. If #6 merges first the appliance ships comments pointing at a note that does not exist. Not a defect in either branch, just a merge order worth stating.


What I verified versus what I inferred

Verified: every dpkg fact above, by dpkg -L, dpkg -S, dpkg-query -W and apt-cache rdepends on the booted wordpress-demo; the common and unit-mariadb file inventory by git ls-files across all 21 repositories; the Mysql_perf_info.py caller; cp's behaviour under -R, -dR and -LR; and that dropping -d from the keel-wordpress test changes no test result.

Inferred: that the five inithooks/confconsole commands are in scope. That is my reading of lines 16-18 applied to packages carrying a +keel version, and it is the reading that makes the rule and the carve-out consistent. If the maintainer intends "a package this project builds" to mean only packages whose name is Keel's, the note should say that, because on the present wording those five are in.

Warning. The HIGH finding is not a reason to hold the decision — the policy itself is sound, well argued, and the three-part justification does the work section 10 asks of it. But the table will be read as the scope, and as it stands it excludes commands the note's own rule includes. I would add the rows or the disclaiming sentence before merge, and take the -d correction at the same time since it is one clause.

… the cp claim

Review of #3 raised one HIGH: the note's inventory contradicted the note's own
scope rule, and in the direction that makes a contributor under-apply the
policy. The rule covers "a script installed by a package this project builds";
the table listed only the two keel-wordpress overlay files, the two in common
and the foreign packages going to tracker#13. So it handled somebody else's
packages and silently dropped the ones we build.

Five commands were missing, all shipping on every appliance: turnkey-init,
turnkey-sudoadmin, turnkey-install-security-updates and
bin/turnkey-init-fence from inithooks, and turnkey-lexicon from confconsole.
Both packages carry a +keel version, both are built from this organization's
forks, and keel-wordpress's Makefile already names them among "the project's
own packages". turnkey-init is the most operator-facing command on the
machine. Two more were missing from common, turnkey-artisan and
turnkey-composer, in scope by the rule though nothing includes laravel.mk or
composer.mk yet; and unit-mariadb carries a byte-identical second copy of
turnkey-mysql-install-perf-info-schemas, same blob, which the table attributed
to common alone, so anybody following it would rename one of two copies.

The inventory is now fifteen names grouped by what puts the file on the
machine, because that is what decides when each can move, with eleven marked
as ours. turnkey-make-ssl-cert from turnkey-ssl is listed too, as explicitly
out of scope, so the next reader does not have to work that out again. And the
section closes by saying the rule wins when the two disagree, since a table is
a measurement of one day and this one has now been corrected three times.

Second finding, a factual error worth more here than in code because a note is
applied without asking: -d is not what preserves the symlink. -P is already
the default under -R and only -L dereferences, so what -d buys is
--preserve=links, which is hard links. As written the note would have taught
that a copy without -d flattens links. Corrected with the measurement, and
keel-wordpress now asserts the real property.

Also from the review: the Decision section says the link is relative and why
an absolute one breaks the build time caller, and that the Keel name is the
turnkey- prefix replaced by keel- and nothing else, both of which were open
questions a reader would have had to guess; the Consequences bullet about
tests says "must be covered" rather than describing a state that is true of
one repository; and the reverse dependency provenance says the container
throughout rather than switching to the build host mid-section.
@marcos-mendez

Copy link
Copy Markdown
Contributor Author

Re-review

I re-measured the inventory rather than reading the new tables, and checked the two claims that carry weight.

The HIGH is resolved

The five commands from packages this project builds are in, as their own group, with the +keel versions that make them ours rather than inherited. The section at 148-156 states the reasoning that was missing, and 40-46 fixes the thing that actually caused the error: the previous heading said "owned by a Debian package", which conflated "packaged" with "somebody else's", and it now says "owned by a package this project does not build" with the +keel test spelled out. That is the correction at the level of the mistake, not at the level of the symptom.

Re-measured independently and the arithmetic holds. Fifteen distinct names, eleven ours: turnkey-wp, turnkey-wordpress-update, turnkey-mysql-install-perf-info-schemas, turnkey-php, turnkey-artisan, turnkey-composer, turnkey-init, turnkey-sudoadmin, turnkey-install-security-updates, turnkey-init-fence, turnkey-lexicon; four not ours: turnkey-sysinfo, turnkey-detect-virt, turnkey-version, turnkey-make-ssl-cert. Justification bullet 1's "thirteen of the fifteen present on a wordpress-demo container, nine of those ours" also matches what the container reports — turnkey-artisan and turnkey-composer are the two absent, correctly, since nothing includes laravel.mk or composer.mk.

The blob claim is exact. git rev-parse gives c8f185b2ab5cdf273d1b9c11b0cf0e2ab073a792 for both common/overlays/mysql/usr/local/bin/turnkey-mysql-install-perf-info-schemas and unit-mariadb/overlay/usr/local/bin/turnkey-mysql-install-perf-info-schemas, and the working files share a sha256. The consequence the note draws at 99-105 is the right one and is the reason that row earns a place in a table it otherwise duplicates.

The turnkey-init-fence paragraph at 158-164 is precise too. git ls-files in inithooks gives exactly what it names: bin/turnkey-init-fence, debian/inithooks.turnkey-init-fence.service, default/turnkey-init-fence, firstboot.d/30turnkey-init-fence, firstboot.d/97turnkey-init-fence-disable and turnkey-init-fence/htdocs/. Every item checks out, and drawing the line at "the compatibility rule covers the command, the unit and conffile names are a packaging question" is the right place to stop a decision note.

turnkey-make-ssl-cert as explicitly out of scope, with turnkey-ssl 3.1.1 carrying no +keel, is correct and worth having written down — an unlisted name and a name listed as out of scope read very differently to the next person.

"When the table and the rule disagree, the rule wins" — genuine, on this evidence

I was ready to read that as a way to keep shipping a wrong table, and on its own it would be. It is not on its own. It arrives with a table that went from five names to fifteen, a stated grouping principle (by what puts the file on the machine, because that is what decides when each can move), and an instruction rather than an excuse: "the right fix is to add the row". A precedence clause that licenses inaction looks different — it appears instead of a measurement, not after a better one.

The clause is also just true. A table is a measurement of one day and this one will go stale the next time a repository gains a command; a policy whose scope is defined by an enumeration is a policy that silently shrinks. Putting the rule above the table is what stops that.

What would make it self-correcting rather than merely honest is the one thing still missing, and it is a LOW suggestion, not a condition. Nothing re-measures. The Consequences bullet already says finding these is grep -rn and not a green build, which is exactly the observation that a five-line check over the organization's trees — every turnkey-* executable, diffed against the names in this note — would turn into something that fails when the table drifts. The note argues for that check without proposing it.

The -d correction

Taken, and stated correctly at 191-210: -P is already the default under -R, -d adds --preserve=links for hard links, and the property the policy depends on is the absence of -L. The measured table with coreutils 9.7 matches what I measured. The paragraph at 198-210 explains why the attribution matters, which is the part that keeps the next person from adding -d where it does nothing.

More to the point, it is no longer only prose. keel-wordpress now has a test that pins it, and I confirmed that test fails when the claim is violated: with a cp shim that dereferences under -R, three tests go red including the one that names this property. The sentence at 219-222 is therefore accurate.

The other items from the first pass

All taken. The relative-link rule and the name-derivation rule are now in the Decision section at 15-22, where a contributor applying the policy will meet them, with the fab-chroot reason attached. "The compatibility names must be covered" replaces the present-tense description, and 279-281 names who owes them. The provenance mismatch is gone — line 70 now says wordpress-demo container rather than build host, which matches where the measurement came from. The closing correction about turnkey-detect-virt no longer frames itself as "five are really six", which was the sentence that made the old table sound settled.

Verified versus inferred

Verified: the blob identity by git rev-parse in both repositories and by sha256; the fifteen/eleven and thirteen/nine counts by re-listing from the repositories and from the container; the turnkey-init-fence file set by git ls-files; the +keel suffixes by dpkg-query -W on the container; the cp semantics by measurement; and that the keel-wordpress test backing 219-222 fails when the property is false.

Inferred: that the precedence clause is a resolution rather than an escape. That is a judgement about intent, and the evidence for it is that the measurement tripled in the same commit.

Approve. Clean above LOW. The single LOW — nothing re-measures the inventory — is a suggestion for a follow-up, not a condition on this note.

Brief section 10 asks for commas, colons, parentheses or semicolons in
documentation; the note carried fourteen em dashes. Wording otherwise
unchanged.
@marcos-mendez
marcos-mendez merged commit e02b6a6 into main Sep 29, 2026
marcos-mendez added a commit that referenced this pull request Sep 29, 2026
…ommands

Decision 0015: operator facing commands carry the Keel name
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant