Compare commits

...
888 Commits
Author SHA1 Message Date
jochen 506d00cf74 Name fix time and fix budget in the glossary (issue 403, ADR 0313)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
mesh/delivery-group group fix/403-fix-time checking: composed check out of date: novox/mesh-controller@7ca5a45ecc5a was judged against trunk ?, the trunk is no…
ADR 0313 makes the span from the move to fixing to delivered the operator's
target; the delivery budget stays a measure inside it, and its leaf figure is
three minutes since ADR 0304.
2026-10-12 15:50:13 +02:00
mesh-admin 285ead0043 Merge pull request 'Give ADR 0313 a home in design 47, the delivery budget it extends' (#330) from chore/adr-0313-home-in-design-47 into main 2026-10-12 13:36:01 +00:00
jochen f63e60a4a1 Give ADR 0313 a home in design 47, the delivery budget it extends
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The knowledge base's whole-set cited rule (to-be 55, ADR 0313's own review)
refuses to accept a record nothing cites. Design 47 already builds on
ADR 0282's delivery budget, which ADR 0313 extends with the operator's
wider fix-time target, so it is the record's home.
2026-10-12 15:29:38 +02:00
mesh-admin 992c075444 Merge pull request 'to-be 55: a drafted clause may be changed, and a clause another record links keeps its number (issue 494, ADR 0321)' (#329) from design/55-a-drafted-clause-may-be-changed into main 2026-10-12 12:18:23 +00:00
jochen 12e59b3c42 to-be 55: a drafted clause may be changed, and a clause another record links keeps its number (issue 494, ADR 0321)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Names ADR 0321 in its home, so the knowledge base's cited rule holds through
the design that carries it.
2026-10-12 14:15:03 +02:00
mesh-admin a3bde3657d Merge pull request 'To-be 47: a covered stop reads covered and stays listed; a one-machine module is covered (issue 480)' (#328) from design/47-a-covered-stop into main 2026-10-12 11:56:36 +00:00
mesh-admin 361d503034 Merge pull request 'Say in to-be 27 how a value several modules share is set once (issue 415, ADR 0309, ADR 0319)' (#327) from design/415-shared-value-trust into main 2026-10-12 11:54:01 +00:00
jochen e8cff1646a Carry ADR 0309 and ADR 0319 in one paragraph of to-be 27, replacing the 0309 paragraph alone (issue 415)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-12 13:50:33 +02:00
jschoubben 94e7e044b9 Say in to-be 47 that a covered stop reads covered and stays listed, and that a one-machine module is covered (issue 480)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-12 13:50:29 +02:00
mesh-admin d0e23b1dbf Merge pull request 'Give ten uncited decisions their homes in the designs they change (issue 504)' (#324) from fix/504-homes-for-uncited-decisions into main 2026-10-12 11:45:28 +00:00
mesh-admin f3cee3e7c9 Merge pull request 'to-be 55: a failed whole-set rule is a warning to the operator and leaves the module healthy; cited is judged in the review (issue 504)' (#325) from fix/504-whole-set-rule-is-a-warning into main 2026-10-12 11:45:03 +00:00
jochen fe4f62c712 to-be 55: a failed whole-set rule is a warning to the operator and leaves the module healthy, and cited is judged in the review (issue 504)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator decided on 2026-10-12, on issue 504, that a rule of the
record set that fails is a fault of the records, not the module failing:
health says only whether the module works (issue 508's principle), so a
finding must not hold a walk at the module's first-node gate.
2026-10-12 13:28:12 +02:00
jochen 23ef14d0ba Give ten uncited decisions their homes in the designs they change (issue 504)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The knowledge base's cited rule failed for ADR 0303, 0306-0312, 0314 and 0315: accepted,
and nothing cited them. Each design that owes the decision now names it in its decisions
and says it in prose, so the records can name these designs as their homes.
2026-10-12 13:24:48 +02:00
mesh-admin ca9d11ea60 Merge pull request 'Adopt a mail account the profile already holds instead of adding it twice (issue 345, ADR 0317)' (#323) from feat/345-adopt-existing-accounts into main 2026-10-12 10:55:13 +00:00
jochen 953038833c Keep recorded accounts by their keys, and name them by place, never by name (ADR 0317 review)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-12 12:45:00 +02:00
mesh-admin 4bd3624c2e Merge pull request 'Say that a condition's looks are kept beside the bucket, not in it (issue 420)' (#321) from fix/420-condition-looks-beside-the-bucket into main 2026-10-12 10:43:27 +00:00
jochen ef452a7219 Name the optional key that settles an ambiguous match, and how the ask shows the accounts (ADR 0317)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
2026-10-12 12:43:23 +02:00
jochen 091ad0aa68 Adopt an account the profile already holds instead of adding it twice (issue 345, ADR 0317) 2026-10-12 12:42:30 +02:00
mesh-admin cad874818a Merge pull request 'Design the mail client set up by the mesh (issue 345, ADR 0316)' (#322) from feat/345-betterbird-set-up-by-the-mesh into main 2026-10-12 10:34:52 +00:00
jochen db92ca73ff Record the operator's answer: Owl's pinned copy stays in the private artifact store (issue 345, ADR 0316)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery delivered
mesh/repo-check pass: its merge-check.sh passed
2026-10-12 12:29:07 +02:00
jochen 2059175b89 Design the mail client set up by the mesh: Betterbird pinned, Owl by policy, accounts without credentials (issue 345, ADR 0316)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-12 12:22:51 +02:00
jochen 3a636a18f2 Say that a condition's looks are kept beside the bucket, not in it (issue 420)
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
to-be 45 section 2's field table put the count of observations, the last observed
time and the evidence in the conditions bucket, which made every look a write and
broke ADR 0289's rule that a bucket is written only on a change.
2026-10-12 12:11:53 +02:00
mesh-admin c22e1b6b80 Merge pull request 'To-be 24 names ADR 0305: a provider hands on the credential its software issued (issue 526)' (#320) from feat/526-a-provider-delivers-what-its-software-issued into main 2026-10-11 23:24:04 +00:00
jschoubben a0291235a2 Say ADR 0305 is accepted in to-be 24, and name the media catalogue among its code (issue 526)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/526-a-provider-delivers-what-its-software-issued delivered: every member is delivered
2026-10-12 01:20:24 +02:00
jschoubben 540f82768e Name ADR 0305's check clause and the late binder's hour in to-be 24 (issue 526)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-12 01:09:13 +02:00
jschoubben 393bac573d Give ADR 0305 its place in to-be 24: a provider hands on the credential its software issued (issue 526)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/526-a-provider-delivers-what-its-software-issued rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
A token a media server got from its vendor could only be typed in per consumer; the vault now takes it
from the provider, sealed to one call, and keeps it as the provision's one credential.
2026-10-12 01:06:06 +02:00
mesh-admin 7ec783dbb0 Merge pull request 'Add Media as the eleventh domain, with its words (ADR 0302)' (#319) from feat/302-media-domain into main 2026-10-11 19:42:59 +00:00
jschoubben 56b7a9084a Add Media as the eleventh domain, with its words (ADR 0302)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
ADR 0302 made Media a domain: content, kind, media release, library,
media file, media-importer, download client, engine and grab now have
one meaning each. Release already meant two things, so the Media sense
is qualified in the homonyms.
2026-10-11 21:32:15 +02:00
mesh-admin e49f772465 Merge pull request 'Give ADR 0299 its home in to-be 47: a stop ends only its own change on a shared walk (issue 459)' (#316) from feat/459-a-stop-re-walks-the-others into main 2026-10-11 19:31:50 +00:00
jschoubben ce67b65c10 Say in to-be 47 that a stop's closed window cuts the batch's unrelated merges early (review of #232, issue 459)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/459-a-stop-re-walks-the-others delivered: every member is delivered
2026-10-11 21:16:05 +02:00
jschoubben f8efb08b3d Say in to-be 47 where a stop's other merges go, what stays on the machines, and that a stopped merge never ships by a walk (ADR 0299's insight, issue 459) 2026-10-11 21:16:05 +02:00
jschoubben d954e34277 Give ADR 0299 its home in to-be 47: a stop ends only its own change on a shared walk (issue 459) 2026-10-11 21:16:05 +02:00
mesh-admin 3d42834908 Merge pull request 'To-be 47 names ADR 0298: the bus's grants arrive before a walk's gate (issue 490)' (#315) from design/47-the-grants-step into main 2026-10-11 19:15:44 +00:00
mesh-admin 41b5723927 Merge pull request 'One word, one thing: the old "knowledge base" uses say what they meant (issue 430)' (#318) from fix/430-one-word-one-thing into main 2026-10-11 19:12:43 +00:00
jschoubben c9d7d03ab8 Keep playbook 03's rule as it was, in plain words, and say how the record is searched (review of #318)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 21:07:32 +02:00
jschoubben c77378fa19 Say what the old 'knowledge base' meant, now that the word names the module (issue 430)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The record and the records module for symptom search, the constitution page by its own name.
2026-10-11 20:54:15 +02:00
mesh-admin 4336fdfb11 Merge pull request 'To-be 53 names ADR 0300: the issue tracker's record is a log of operations' (#317) from design/53-adr-0300-home into main 2026-10-11 18:41:56 +00:00
jschoubben f95f758c87 Give ADR 0300 its place in to-be 53: the tracker's record is a log of operations (issue 508)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
ADR 0300 was accepted with no home, so the knowledge base's whole-set rule fails and holds every rollout of mesh-knowledge.
2026-10-11 20:38:02 +02:00
jschoubben 34bdb1a34f Give ADR 0298 its place in to-be 47: the bus's grants arrive before a walk's gate (issue 490)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 19:22:05 +02:00
mesh-admin e4f7439ea4 Merge pull request 'Glossary and to-be 53 for ADR 0294: a linked fix, withdrawn, abandoned (issue 483)' (#313) from decisions/483-a-linked-fix-is-measured into main 2026-10-11 16:47:28 +00:00
jschoubben c3cc29180e Name a linked fix, withdrawn and abandoned in the glossary, and give ADR 0294 its place in to-be 53 (issue 483)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
ADR 0294 itself now lives in the knowledge base, proposed there, since the records left hq (issue 426).
2026-10-11 18:01:46 +02:00
mesh-admin 22ffe073bf Merge pull request 'Issue 426, ADR 0290 rule 9: the decision records and research efforts leave hq for the knowledge base' (#312) from feat/426-the-records-leave-hq into main 2026-10-11 11:27:11 +00:00
jschoubben 0afc8cc5f8 Name the commit the knowledge base's records now come from, and link research with touches and decisions with rests-on (review of #312)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
2026-10-11 13:25:21 +02:00
jschoubben 418261213c Remove the decision records and research efforts, which the knowledge base now holds whole (issue 426)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
ADR 0290 rule 9: once the import is checked record by record, hq keeps no copy.
The records and efforts are removed; every link or path to them reads "ADR NNNN" or
"research NNN"; a design's decisions: lists "ADR NNNN". 02-DECISIONS keeps a pointer
README with the reading order's six topics, which the knowledge base reads through the
records module; 01-RESEARCH keeps a pointer README. The checks drop the rules that
judged records (the knowledge base holds each at propose; checks/README.md says where)
and refuse a record written here again. Playbooks 01 and 02 and the skills write and
read records with the knowledge base's verbs.
2026-10-11 12:56:09 +02:00
mesh-admin 76b346afcd Merge pull request 'To-be 55: rests-on and touches links, the import's refresh; ADR 0248's index comes from the knowledge base' (#314) from design/55-links-and-refresh into main 2026-10-11 10:52:43 +00:00
jschoubben cd8b4c5e5d ADR 0290: say on whose word rule 8 gained refresh, and name the same versions as to-be 55
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 12:38:49 +02:00
jschoubben 4ea4cf6529 ADR 0290 rule 8 and to-be 55: a refresh repairs drafted records too, as a new version
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery superseded: a newer head of the same pull request
mesh/repo-check pass: its merge-check.sh passed
2026-10-11 12:38:04 +02:00
jschoubben d2de5ddf8c To-be 55 and ADR 0248: say exactly what the links and refresh do, and why the index moved
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 12:30:39 +02:00
jschoubben 510f7aecf1 To-be 55: the rests-on and touches links and the import's refresh; ADR 0248's index now comes from the knowledge base
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-11 12:29:32 +02:00
mesh-admin f08920199d Merge pull request 'ADR 0290: the knowledge base reviews a proposal itself' (#311) from decisions/0290-the-knowledge-base-reviews-itself into main 2026-10-11 09:27:05 +00:00
jschoubben d5eaba1359 ADR 0290: the knowledge base reviews a proposal itself, through a fresh judgement, since the mesh proves machines and not agents
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 11:20:55 +02:00
mesh-admin 331e8d0bbe Merge pull request 'Proposed: ADR 0293, an ask the controller made is delivered again to its seat's worker (issue 334)' (#310) from decisions/334-the-controllers-own-ask-is-delivered-again into main 2026-10-11 02:36:25 +00:00
jschoubben f9677dabea Accept ADR 0293 on the operator's word, and give it its place in to-be 45 (issue 334)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator chose 2026-10-11 to let the controller deliver its own given-up asks again; every other ask stays drop-only.
2026-10-11 04:21:12 +02:00
mesh-admin d1edcf544b Merge pull request 'ADR 0290: hq keeps no copy of the records (issue 435)' (#308) from decisions/0290-no-copy-in-hq into main 2026-10-11 01:39:30 +00:00
jschoubben ab947a8c1c Propose ADR 0293: the controller delivers its own given-up asks again (issue 334)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 03:25:53 +02:00
mesh-admin 7b5053ba05 Merge pull request 'ADR 0292: a consumer of the part a provider waits on is held under the wait, and passes its gate as a wait for a person (issue 405)' (#309) from decisions/405-a-waiting-providers-consumer-passes into main 2026-10-11 01:23:31 +00:00
jschoubben 2ea803acce ADR 0292: quote ADR 0283 decision 5 exactly, and weigh a declared part-to-provision mapping
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 03:11:25 +02:00
jschoubben 267a04ccad Point ADR 0240 rule 5 and ADR 0283 decision 1 at ADR 0292, and give it its home in to-be 48 (issue 405)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 03:09:14 +02:00
jschoubben a6b08de850 ADR 0292: a consumer of the part a provider waits on is held, and passes its gate as a wait for a person (issue 405)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check fail: its merge-check.sh failed: 02-DECISIONS/0292-a-consumer-of-the-part-a-provider-waits-on-is-held-under-the-wait-and-passes-its-gate-as…
2026-10-11 03:08:47 +02:00
jschoubben 0aa37ad8b0 Note on ADR 0240 rule 5 and ADR 0283 decision 1: what a provider waiting for the operator holds (issue 405)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 03:06:01 +02:00
jschoubben 11b719fd5c ADR 0291: the decision an issue led to stays in the knowledge base
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 02:10:36 +02:00
jschoubben 040316b126 ADR 0290 and 0291: the review's wording, and a dated note on 0291
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 02:10:03 +02:00
jschoubben 594dd9ce51 ADR 0290: hq keeps no copy of the records once they leave it, on the operator's word (issue 435)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-11 02:09:01 +02:00
mesh-admin 07ecb39a2e Merge pull request 'ADR 0290 rewritten: a decision record lives whole in the knowledge base' (#307) from decisions/0290-records-whole-in-the-knowledge-base into main 2026-10-11 00:07:48 +00:00
jschoubben d188941e73 Checks README: a tracking issue that lives only in the tracker counts, and what that cannot catch (issue 436)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 02:07:01 +02:00
jschoubben a5d1e0fbf6 ADR 0290 rewritten on the operator's word: a decision record lives whole in the knowledge base, reviewed and accepted there; and cycle.py sees issues that live only in the tracker (issue 436)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 02:05:02 +02:00
mesh-admin 08b1077b51 Merge pull request 'ADR 0290 and 0291: the knowledge base, and an issue whole in the issue tracker (research 045)' (#306) from decisions/0290-0291-the-knowledge-base-and-issues-whole-in-the-tracker into main 2026-10-10 23:26:51 +00:00
jschoubben 63dcd116e8 Glossary: the two senses of knowledge base below the senses of record, not among them
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 01:25:47 +02:00
jschoubben b174269ec1 ADR 0290 and 0291: the operator's confirmations, no folder rule before issue 427, the verb return, the documents' row in to-be 53, and both senses of knowledge base
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 01:25:11 +02:00
jschoubben 4c4d96874f ADR 0291: the choice of no export is this record's, not an answer the operator gave
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 01:02:02 +02:00
jschoubben d606b4382f ADR 0290 and 0291: the knowledge base holds each record's structure and clauses, and an issue lives whole in the tracker (research 045)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 01:01:42 +02:00
jschoubben e412b26987 Claim decision numbers 0290 and 0291 for research 045's graduation 2026-10-11 00:56:45 +02:00
mesh-admin 7b8ab6bcc5 Merge pull request 'Research 045 findings: a knowledge base agents ask instead of reading markdown' (#304) from research/045-a-knowledge-base-the-mesh-writes into main 2026-10-10 22:55:04 +00:00
mesh-admin ca90258f0d Merge pull request 'Declare every partial supersession in both records, and check it' (#305) from fix/loose-links-between-decision-records into main 2026-10-10 22:53:42 +00:00
jschoubben e41fd0de1d Research 045: the knowledge base holds no counter of its own, and the cleanup is in review
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 00:53:27 +02:00
jschoubben 9631b17d16 Research 045: numbers stay with the issue tracker as ADR 0275 decided, and the review's corrections
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 00:52:41 +02:00
jschoubben f607598898 Check a partial supersession's date and refuse the older note form, as the review of this change found 0269's note on 0118 went round the check
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-11 00:52:13 +02:00
jschoubben fd0b3b9976 Declare every partial supersession in both records and check it, so a reader who follows a citation to the older record learns what no longer holds
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 00:47:07 +02:00
jschoubben 6a408428d6 Write every extends target with its folder, as the frontmatter template says, so a reader of the field needs no second form 2026-10-11 00:43:13 +02:00
jschoubben 91ae2ddb0f Research 045: the store holds each record's structure and rules, hq its reasoning, and issues live whole in the tracker
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 00:41:59 +02:00
jschoubben fcf2fd0b46 Research 045 findings: serve the record as answers, keep review for what governs, and give light records a home
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-11 00:30:13 +02:00
mesh-admin eac30272ee Merge pull request 'ADR 0283: the username lives in its sources entry, and the needs-operator words as built (issue 386)' (#297) from fix/386-username-in-sources into main 2026-10-10 20:25:24 +00:00
mesh-admin 86715ee5c1 Merge pull request 'ADR 0285: every condition's message says which issue tracks it and where it stands (issue 400)' (#299) from feat/400-alert-names-its-issue into main 2026-10-10 20:12:38 +00:00
jochen 8f99511160 Check every rule of ADR 0285, point ADR 0253 at it under its verdict rule, and cite the pull requests
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/400-alert-names-its-issue delivered: every member is delivered
Review of hq#299.
2026-10-10 22:09:24 +02:00
jochen c79e416757 ADR 0285: say in every condition's message which issue tracks it and where it stands (issue 400)
The operator could not tell from a notification whether its cause was
known, tracked and being fixed. An issue tracks a condition by its key;
the tracker answers which issues track which conditions, and the router
says it in every message about the condition. Amends to-be 53 and 45,
playbook 03 and the glossary, and points ADR 0253 at the new line.
2026-10-10 22:09:24 +02:00
mesh-admin 1b84960179 Merge pull request 'ADR 0289: the controller mirrors its records into buckets written only on a change (issue 419, research 036 G2)' (#303) from decisions/0289-the-controller-mirrors-its-records-on-the-bus into main 2026-10-10 20:07:05 +00:00
jochen 9be659b18c Say how the writer tells its own lost acknowledgement from a former holder, list D16, and fix the settings heading (review of #303)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 22:05:17 +02:00
jochen 5a3513b819 Keep the epoch out of a mirror's value, age purge markers, say a lost event, and check every rule (review of #303) 2026-10-10 22:05:17 +02:00
jochen f90b74bf26 ADR 0289: the controller mirrors its records into buckets written only on a change (issue 419, research 036 G2)
Nodes, assignments, walks, builds and settings could be seen only by polling verbs, and a setting
changed with no event. Each becomes a record mirror: a bucket written from the store after the commit
and only when the value differs, one event per write, compared with the store on every self-check run
(D17, mirror-drifted, healer H6). The write-only-on-a-change rule from issue 326 now holds for every
bucket; the conditions bucket breaks it and issue 420 tracks that. The effort stays active for G1, G3
and the views.
2026-10-10 22:05:17 +02:00
mesh-admin 750a1d54d4 Merge pull request 'ADR 0286: a tool or verb marked in its manifest runs only on the operator's approval of that one call (issue 417)' (#302) from feat/0286-approval-bound-call into main 2026-10-10 20:04:24 +00:00
jochen 8060c20fda Separate the release paragraph from the execute bullet, and say the third call asks again
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 22:00:27 +02:00
jochen 1d6ee8885a Bind old runners through the bus, say when the same call asks anew, and leave release unmarked until the controller's warrant passes the mark (review) 2026-10-10 22:00:27 +02:00
jochen 9b0c6741e7 ADR 0286: a tool or verb marked in its manifest runs only on the operator's approval of that one call (issue 417)
Graduates research 039. The tool runner (and the controller for its own verbs) asks the operator
through the existing operator-channel asks and runs the exact call once on the warrant, reusing the
act digest and warrant check of ADR 0259 and the shown-in-shape form of ADRs 0274 and 0277, so
risky agent calls stop resting on the agent's restraint. Amends to-be 46 and hands it off.
2026-10-10 22:00:27 +02:00
mesh-admin 8932e93a4b Merge pull request 'ADR 0287: a seat names the paths its holder owns, and a claim without them is refused (issue 412, research 023)' (#301) from feat/grad-023-seat-paths into main 2026-10-10 19:59:46 +00:00
jochen 906d702b8d Narrow ADR 0287's claimant rule to any seat naming the path, and correct two facts the review found
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 21:57:24 +02:00
jochen 6b49ed613c ADR 0287: a seat names the paths its holder owns, so a file's owner is read from the seat (issue 412, research 023) 2026-10-10 21:57:24 +02:00
mesh-admin 3cce6dda4b Merge pull request 'ADR 0288: the object store stays on the maintained fork, and Garage is chosen only after the consumers' S3 features are counted (issue 413)' (#300) from decisions/0288-the-object-store-after-minio into main 2026-10-10 19:54:13 +00:00
jochen fb2d5d9f7f Name what checks the fork's triggers and track the mirroring question and the missing kept-by (issues 421, 413)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 21:52:00 +02:00
jochen b6c592cb9d Record the maintained MinIO fork as the object store, and choose Garage only after the consumers' S3 features are counted (research 015, issue 413) 2026-10-10 21:52:00 +02:00
mesh-admin 16cfe6d8c3 Merge pull request 'Close ten research efforts their decisions already settled (issue 408)' (#298) from fix/408-close-settled-research into main 2026-10-10 19:51:42 +00:00
jochen 907051c21d Keep 012 and 027 open and give every leftover of the closed efforts an issue (issue 408)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The review found questions no record decides: 012's minimum-viable-node
half and 027's packaging, secrets, sizing and leftovers questions, so both
stay active. Where the record lives (006) is issue 414, and 037's two
other leftovers are issues 415 and 416; to-be 06 now points at issue 414.
2026-10-10 21:48:28 +02:00
jochen 9a2a2006b1 Cover ADR 0283's check row 7 with the insight, and align to-be 48 with the words as built
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
From the review of hq#297.
2026-10-10 21:44:11 +02:00
jochen f515d8b6b2 Close twelve research efforts their decisions already settled (issue 408)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Ten graduated and two abandoned efforts still said active, so the record
read as open questions that later ADRs and designs had answered. Each now
names in became: what decided or overtook it. To-be 45's undecided item
moves from research 017 to issue 409, and research 037's shell follow-up
is issue 410.
2026-10-10 21:43:53 +02:00
jochen a3ac9fd547 Carry an SMB source's username in its sources entry, since a key naming a user may have no default (issue 386)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
The live module check refused mesh-catalog#190's smb-users setting (ADR 0262);
ADR 0283 decision 7 gets a dated insight, to-be 52 and the 0263 note follow.
2026-10-10 21:42:54 +02:00
mesh-admin 90666a4ac3 Merge pull request 'Renumber the fixing-status record to ADR 0284 (issue 406)' (#296) from fix/renumber-0283-fixing-to-0284 into main 2026-10-10 19:42:41 +00:00
jochen ace8f6c398 Renumber the fixing-status record to ADR 0284 (issue 406)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
hq #294 (issue 386) and #295 (issue 399) both merged a record numbered 0283;
#294 landed first and keeps it. The branch that lands last renumbers.
2026-10-10 21:35:34 +02:00
mesh-admin 61255b56d4 Merge pull request 'ADR 0283: an agent moves an issue to fixing under its own name, and an issue is resolved only from fixing (issue 399)' (#295) from feat/399-fixing-status into main 2026-10-10 19:13:24 +00:00
mesh-admin 536b193c7b Merge pull request 'ADR 0283: a part waiting for the operator's secret or setting is said as waiting and passes the first-node gate (issue 386)' (#294) from feat/386-a-wait-for-the-operator into main 2026-10-10 19:10:46 +00:00
jochen 2648d571b3 Point ADRs 0240 and 0254 at ADR 0283, bound a family member's name, and say the cost of a setting wait
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/386-a-wait-for-the-operator delivered: every member is delivered
From the review of hq#294.
2026-10-10 21:08:40 +02:00
jochen 8cdc9706e1 Use role names, not node names, in the worker examples (public repository)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/399-fixing-status delivered: every member is delivered
2026-10-10 21:08:25 +02:00
jochen 21973e2e4a ADR 0283: an agent moves an issue to fixing under its own name, and resolves only from fixing (issue 399)
mesh/delivery-group group feat/399-fixing-status checking: 0 of 2 member(s) ready
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Two agents could start on one issue without seeing each other, and an issue
could be resolved without anyone having said they started its fix. Adds the
status fixing, the issue's worker (by, who@node) with take-over and a stale
bound, and resolved only from fixing; settles what to-be 53 left out as
claims, under the new glossary word worker. Amends to-be 53, playbook 03 and
the hq-diagnose and hq-status skills, with pointer notes on ADR 0275 and 0281.
2026-10-10 21:07:13 +02:00
jochen 242a6c548b ADR 0283: a part waiting for the operator's secret or setting is said as waiting and passes the first-node gate (issue 386)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/386-a-wait-for-the-operator checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
A module whose only unhealthy part waited for the operator's secret failed
its gate, held every later walk and told the operator there was nothing to
do. The record adds a waits answer and a waiting state, checked by the
controller, read by the gate as waits for a person, and said as a
needs-operator condition naming the act; and secret families so each SMB
source's password is one hidden line at the desk prompt.
2026-10-10 21:05:29 +02:00
mesh-admin 5f4ff3a80b Merge pull request 'To-be 47: handed to mesh-controller and mesh-delivery for ADR 0282's first slice (issue 382)' (#291) from design/47-handoff-delivery-budget-slice-1 into main 2026-10-10 18:16:55 +00:00
mesh-admin 9e6f78f5eb Merge pull request 'Accept ADRs 0164-0166 and cite them in to-be 42 (issue 398)' (#293) from decisions/accept-0164-0166 into main 2026-10-10 18:04:25 +00:00
jochen 15d5383a94 Accept ADRs 0164-0166 and give them a home in to-be 42
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator accepted them on 2026-10-10. The runtime seat and the default
half of declared settings are live; the rest is tracked by issue 398. To-be 42
cited them only as waiting, so it now names them.
2026-10-10 20:03:04 +02:00
mesh-admin c6ed41c5ab Merge pull request 'Accept ADR 0242: a recorded build moves only by a person's push' (#292) from decisions/accept-0242 into main 2026-10-10 18:02:10 +00:00
jochen a3e962f880 Accept ADR 0242: a recorded build moving only by a person's push is built and live
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh-controller #115 and mesh-catalog #106 shipped it (issue 295 resolved);
upgrade shows mailu as record under this ADR, and to-be 44 and 45 cite it.
2026-10-10 20:00:40 +02:00
jochen 2995ea9e05 Hand to-be 47 over to its builders: ADR 0282's first slice starts in the controller and mesh-delivery
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 19:59:33 +02:00
mesh-admin 8eca7b921e Merge pull request 'ADR 0282 (proposed): a leaf fix runs healthy on every node within five minutes of its merge, a core module within ten' (#289) from decisions/0282-a-leaf-fix-runs-healthy-everywhere-within-five-minutes-of-its-merge into main 2026-10-10 17:59:02 +00:00
jochen 0a6172640c Accept ADR 0282: the operator decided the delivery budgets on 2026-10-10
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
To-be 47 lists the record among its decisions, ADR 0236 and ADR 0276 say
where their mechanism moves, and the glossary drops 'proposed'.
2026-10-10 19:57:07 +02:00
mesh-admin e08aaae484 Merge pull request 'Research 044: the operator's direction, a mesh-monitor module, is the leading hypothesis' (#290) from research/044-mesh-monitor-hypothesis into main 2026-10-10 17:56:56 +00:00
jochen 8b6c306cfd ADR 0282: a core send sets aside a leaf judgement; the window row says how it was split; the repository rule cites ADR 0276
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
From the independent review: a core delivery could wait on a leaf judgement past
its ten minutes, the window figure's split was not stated beside it, and the
one-repository rule was credited to the wrong issue.
2026-10-10 19:53:36 +02:00
jochen f2250b4471 Research 044: the operator's direction, a mesh-monitor module, is the leading hypothesis
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 19:52:05 +02:00
jochen 9414855fbe ADR 0282 (proposed): a leaf fix runs healthy on every node within five minutes of its merge, a core module within ten
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator asked that a bug fix land on every node in under five minutes. 51
walks measured from the controller's own records put a leaf walk at a median of
6.0 minutes (7 of 20 within five) and say where the time goes: waiting behind
another walk, the first-node judgement, the merge window. The record weighs each
lever by what it would have saved, keeps three readings for core modules (ten
minutes, the measured worst being 8.8), and proposes the measure and condition
that check it. To-be 47 gains the section, the owners and five slices; the
glossary gains delivery time, delivery budget, core and leaf module.
2026-10-10 19:51:07 +02:00
mesh-admin 0cabe5d391 Merge pull request 'Research 045: a knowledge base the mesh writes (decisions, guidelines, policies)' (#288) from research/045-a-knowledge-base-the-mesh-writes into main 2026-10-10 16:17:34 +00:00
mesh-admin 3c323370b7 Merge pull request 'Research 044: what leaves the controller next' (#287) from research/044-what-leaves-the-controller-next into main 2026-10-10 16:17:30 +00:00
jochen 72f940d2c7 Open research 045: a knowledge base the mesh writes
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The record is read-only and hand-written; numbers collide and private knowledge
has no home. Framed as questions, weighing git as the store against a store
with hq as its public export.
2026-10-10 18:14:29 +02:00
jochen c148066de4 Open research 044: what leaves the controller next
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Every controller change walks the core's tier and holds up every other walk
(issue 382), while most of the week's change sat in one package. Framed as
questions: which domain changes most and needs the core least.
2026-10-10 18:14:28 +02:00
mesh-admin 62efd13e07 Merge pull request 'ADR 0281, to-be 53 §11: an issue knows whether its fix is delivered, and a fix stuck after its merge is loud' (#286) from decisions/0281-an-issue-knows-whether-its-fix-is-delivered into main 2026-10-10 16:13:06 +00:00
jochen 0336f4f04b To-be 53: decisions in order; what it leaves out names the notifications it now has (review)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 18:11:46 +02:00
jochen 5ef7d4dc36 ADR 0281, to-be 53 §11: an issue knows whether its fix is delivered, and a fix stuck after its merge is loud
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator approved measuring, from the delivery's own events, whether each fix
an issue names reached the machines; delivered is not resolved, and a merged fix
that does not arrive must be said rather than found.
2026-10-10 18:08:38 +02:00
mesh-admin d836092329 Merge pull request 'ADR 0279, to-be 53 §10: the issue tracker triages every new issue, and an issue is ready when what it depends on is delivered' (#285) from decisions/0279-the-issue-tracker-triages-every-new-issue into main 2026-10-10 16:00:22 +00:00
jochen d5aecc7a31 ADR 0279: set says who set a value in its event; a value is changed, not cleared
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 17:54:55 +02:00
jochen 6b718eeca3 ADR 0279: a wontfix duplicate lets go only with its original; ready said once per spell; a delivery not asked is loud (review) 2026-10-10 17:54:48 +02:00
jochen 3e346e7cd7 ADR 0279: ready waits for the pull requests fixed-by names, not every entry
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-10 17:49:42 +02:00
mesh-admin ccbe5a00b3 Merge pull request 'ADR 0275 slice 3: issue reports carry only opened; the tracker holds every status' (#283) from process/issue-reports-carry-only-opened into main 2026-10-10 15:48:29 +00:00
jochen daa9f171ae Issue reports carry only opened: the tracker holds every status, so no copy can drift
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The import of slice 3 (ADR 0275) was checked against every folder: no report changed since the
commit it read, and the tracker holds each report's status, owner, fix, design amendment, replay
and closing day. The keys are removed from all 361 reports, cycle.py refuses them in any report,
and the playbooks, skills and READMEs that still pointed at them name the tracker instead.
2026-10-10 17:44:18 +02:00
jochen fa6acb04c9 ADR 0279, to-be 53 §10: the issue tracker triages every new issue, and an issue is ready when what it depends on is delivered
The operator asked for severity and what an issue needs to be estimated by the
judgement that already reads the backlog, and for dependencies between issues,
an issue being workable only once what it waits for is on the machines.
2026-10-10 17:38:06 +02:00
mesh-admin d82dde3dca Merge pull request 'ADR 0278, to-be 53 §9: the issue tracker checks every new issue for a duplicate' (#281) from decisions/0278-the-issue-tracker-checks-each-new-issue-for-a-duplicate into main 2026-10-10 15:22:34 +00:00
jochen 5f2f520018 ADR 0278: the check's worst case is under the tool runner's two minutes, not one
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 17:21:39 +02:00
jochen f89c2c350d ADR 0278: the check as a whole takes up to a minute, kept alive by the tool runner (issue 387); one judgement at a time
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Review found the two questions together exceed the consumer's ack wait, which the
tool runner did not extend; the record says so and names the fix it waits on.
2026-10-10 17:20:00 +02:00
jochen cf3065c660 ADR 0278, to-be 53 §9: the issue tracker checks every new issue for a duplicate
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator asked for duplicates to be consolidated, and better, when an issue is
opened rather than on a schedule. A judgement needs a model, and the mesh had no
channel for one; the agent module answers one question with no tools, under the
node's licence, and the tracker acts only on a clear duplicate, closing the newer
issue and keeping its report on the older one.
2026-10-10 17:04:27 +02:00
mesh-admin 894be9971f Merge pull request 'An issue's status lives in the issue tracker: playbook 03, cycle.py and the notes slice 3 of ADR 0275 asked for' (#280) from process/issue-status-lives-in-the-tracker into main 2026-10-10 14:17:46 +00:00
jochen 6d1310f1ea An issue's status lives in the issue tracker: playbook 03, cycle.py and the notes slice 3 of ADR 0275 asked for
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 15:54:32 +02:00
mesh-admin 1ea678c6bd Merge pull request 'ADR 0277: a trusted setting is proposed through a verb and set on the operator's warrant, and a module's own secret is asked for and typed at the desk' (#278) from decisions/0277-a-trusted-setting-is-proposed-and-set-on-the-operators-warrant into main 2026-10-10 12:50:19 +00:00
mesh-admin 76bb721cf4 Merge pull request 'Issue 362 resolved: ADR 0276 gains the own-path batch rule and a progressive insight, to-be 45 gains S18 and S19' (#277) from issues/362-resolved into main 2026-10-10 12:43:19 +00:00
jochen 3539446885 ADR 0277: the migrations are 0090 and 0091, and the code lands in two pull requests
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 14:42:47 +02:00
jochen 4d92d3872b ADR 0277: a trusted setting is proposed through a verb and set on the operator's warrant, and a module's own secret is asked for and typed at the desk
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator does not want to run manual commands on the control node. Issue 339 made every trusted
setting the terminal's alone, and a module's own secret the terminal's or the give verb's: each a line
an agent could only hand the operator to type. ADR 0274's pattern is extended: an agent proposes, the
operator approves on a channel that proves who answered, the controller sets the layer on the warrant
bound to the exact values shown. A secret is asked for by a verb and typed at the desk, bounded, the
prompt naming who asked. The designs 44, 52, 46 and 13, issue 339, ADR 0259 and 0266 and the glossary
are amended with dated notes.
2026-10-10 14:24:06 +02:00
jochen 952c984c34 Issue 362 resolved: ADR 0276 gains the own-path batch rule and a progressive insight, to-be 45 gains S18 and S19
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 14:14:56 +02:00
mesh-admin a5ff354eee Merge pull request 'ADR 0274, progressive insight: the router judges whether a channel can carry the ask (issue 370)' (#273) from decisions/0274-insight-the-router-judges-the-channel into main 2026-10-10 11:11:29 +00:00
mesh-admin 8af56cf2ae Merge pull request 'Playbook 03 takes an issue's number from the issue tracker' (#274) from process/issue-numbers-from-the-tracker into main 2026-10-10 11:09:15 +00:00
jochen beaeedca79 Playbook 03 takes an issue's number from the issue tracker
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Issue numbers collided while each author read main and the open pull
requests; since 2026-10-10 the tracker (ADR 0275) hands them out. The
playbook and the hq-new-issue skill now open every issue there, keep a
folder only for a design or governance issue, and say where its status
lives until slice 3. Research 042's finding that the SDK had no
compare-and-set is corrected, and to-be 53 names the module's code.
2026-10-10 13:06:13 +02:00
jochen 91f2a61763 ADR 0274, progressive insight: the router judges whether a channel can carry the ask
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-10 13:04:22 +02:00
mesh-admin cf529b567c Merge pull request 'ADR 0275, to-be 53: the issue tracker's module is mesh-issues, holding the seat issue-tracker' (#272) from decisions/0275-the-issue-trackers-module-is-mesh-issues into main 2026-10-10 10:59:54 +00:00
jochen 8d9eae7224 ADR 0275, to-be 53: the issue tracker's module is mesh-issues, holding the seat issue-tracker
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
The operator named the module on 2026-10-10: the mesh's own parts carry the
mesh- prefix, and the seat is the role. A dated insight in ADR 0275 says so,
and that compare-and-set state was already in the runtime (ADR 0259): slice 1
added the SDK's Increment. To-be 53 and the glossary say the same, and the
design is handed to mesh-catalog and mesh-sdk (in progress).
2026-10-10 12:52:37 +02:00
mesh-admin 2798306de7 Merge pull request 'ADR 0276: merges are assembled in a rolling window, and each batch is delivered by one walk (issue 362)' (#271) from decisions/0276-merges-are-assembled-in-a-rolling-window into main 2026-10-10 10:44:53 +00:00
jochen d159bfa36a ADR 0276: merges are assembled in a rolling window, and each batch is delivered by one walk
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Two catalogue merges 18 s apart opened two walks; the second superseded the
first and nothing gave it its word for 58 minutes (issue 362). The operator
decided that merges are collected in a rolling merge window (90 s, at most
10 min) and each batch is walked once, one commit per repository (the later
one of a repository, which contains the earlier), one walk at a time; the
walk-to-delivery bookkeeping is repaired too and every wait has a condition.
Amends to-be 47 and 10, adds merge window and batch to the glossary, and
corrects the issue's account of the S16 condition.
2026-10-10 12:39:13 +02:00
mesh-admin 9395c752cc Merge pull request 'ADR 0275 and to-be 53: the mesh numbers and tracks its issues in a module of its own' (#270) from decisions/the-mesh-keeps-its-own-issues into main 2026-10-10 10:34:51 +00:00
jochen 015ff1adfb Graduate research 042 into ADR 0275 and to-be 53, so issue numbers stop colliding
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Numbers taken by reading a repository many branches write at once collide;
one counter in a module of the mesh's own, advanced by compare-and-set,
ends that at the source. The operator delegated the five open questions
on 2026-10-10; each is decided as the research recommended. The glossary
gains the issue tracker, retires "ticketing system" and lists ticket as a
homonym, since ADR 0258 already uses it for an answer's one-time token.
2026-10-10 12:31:32 +02:00
mesh-admin 835d7acc1a Merge pull request 'ADR 0274: an agent proposes a shared instruction section, and every node takes it only on the operator's warrant' (#268) from decisions/0274-shared-instructions-on-the-operators-approval into main 2026-10-10 10:09:39 +00:00
jochen b18bf66ae2 ADR 0274 accepted, with its two limits: to-be 36 rests on it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 12:06:51 +02:00
jochen a211644102 ADR 0274: variation selectors, blank letters and every heading form refused 2026-10-10 12:06:38 +02:00
jochen 842ed805cc ADR 0274: hold the review's findings — invisible text, any @, the record judged at decide, older approvals never undo later ones 2026-10-10 12:06:38 +02:00
jochen 891608814e ADR 0274: an agent proposes a shared instruction section, and every node takes it only on the operator's warrant
The operator asked for mesh tools to add shared instructions and context. Issue 340 made that text the
terminal's until a surface of the operator's own exists; the operator channel is that surface. Proposed,
for the operator to accept.
2026-10-10 12:06:38 +02:00
mesh-admin 7c866e7356 Merge pull request 'Issue 364: the merge gate refuses a module that uses a seat another module declares' (#269) from issues/364-the-gate-refuses-a-module-that-uses-a-seat-another-declares into main 2026-10-10 02:05:20 +00:00
jochen 7b096f2471 Issue 364: the merge gate refuses a module that uses a seat another module declares
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-10 03:53:06 +02:00
mesh-admin d457921486 Merge pull request 'Issue 363: ADR 0267 was decided and never built' (#266) from issues/363-adr-0267-was-decided-and-never-built into main 2026-10-10 01:34:44 +00:00
mesh-admin 014554ce9b Merge pull request 'Research 042: the mesh keeps its own issues' (#267) from research/042-the-mesh-keeps-its-own-issues into main 2026-10-10 01:02:22 +00:00
mesh-admin 591731ab54 Merge pull request 'Issue 341: a merge check judges the pull request's head, not what merging it makes' (#242) from issues/341-a-merge-check-judges-the-head-not-the-merge into main 2026-10-10 01:02:09 +00:00
mesh-admin b106518cda Merge pull request 'Issue 338: a module built from a shared repository moves on every merge to it' (#231) from issues/338-a-module-built-from-a-shared-repository-moves-on-every-merge-to-it into main 2026-10-10 01:01:58 +00:00
jochen 028f3b6ae5 Research 042: the mesh keeps its own issues
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Opens the effort the operator asked for on 2026-10-10: an issues module on the bus that hands out
numbers atomically, so agents stop colliding on them, and takes in the 357 issue folders. Findings
cover the first slice, where the truth lives, the import, conditions, views, and hits as a model.
2026-10-10 02:58:37 +02:00
jochen f45f5f6f20 Issue 363: ADR 0267 was decided and never built, so a controller merge still moves three modules
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator took the rule as live. Track its build, as issue 354 does a deferred rename.
2026-10-10 02:46:35 +02:00
mesh-admin 503e144ab6 Merge pull request 'Issue 361: the setuid search shows its progress, and the operator can ask for a fresh one at the terminal' (#265) from issues/361-the-setuid-search-shows-its-progress into main 2026-10-10 00:06:34 +00:00
jochen 2d0a6ba4bc Issue 361: a setuid find counts only where the agent account can execute it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 02:00:09 +02:00
jochenandClaude a8e9a9afd4 Issue 361: the setuid search shows its progress, and the operator can ask for a fresh one at the terminal
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-10 01:55:52 +02:00
mesh-admin c2d98bc61b Merge pull request 'ADR 0266: the control node's agent account is named nox' (#264) from decisions/0266-the-agent-account-is-named-nox into main 2026-10-09 23:32:58 +00:00
mesh-admin 851a5e9f45 Merge pull request 'Issue 362: two merges seconds apart left their delivery waiting for a person' (#263) from issues/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person into main 2026-10-09 23:32:52 +00:00
jochen 997e422a32 ADR 0266: the control node's agent account is nox, the operator's choice; the ways not judged are tracked by issue 344
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 01:29:42 +02:00
jochen 496452fb54 Issue 362: two merges seconds apart left their delivery waiting for a person, because a walk at a later commit matched no live delivery; issue 349 points at it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 01:28:33 +02:00
mesh-admin 511cea04f2 Merge pull request 'Issue 361: the control-node's agent account is never judged, because its search for setuid programs cannot finish in fifteen minutes' (#261) from issues/361-the-control-nodes-agent-account-is-never-judged into main 2026-10-09 23:18:48 +00:00
jochen 94719d4662 Issue 361: say when a search runs as the fix does it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-10 01:12:19 +02:00
jochen 46f89cd102 Issue 361: record the review of the fix and what it changed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-10 01:12:02 +02:00
mesh-admin 288da29d94 Merge pull request 'Research 041: the agent's session runs confined, instead of being proven by a disk-wide search' (#262) from research/041-the-agents-session-runs-confined into main 2026-10-09 23:07:33 +00:00
jochen 4bc0919102 Research 041: the agent's session runs confined, instead of being proven by a disk-wide search
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The setuid search did not finish within its bound on the control-node, so no
approval proven there is honoured; a confined start may answer the question
ADR 0259 asks directly, at a cost that does not grow with the disks.
2026-10-10 01:04:02 +02:00
jochen c0c7515e06 Issue 361: the control-node's agent account is never judged, because its search for setuid programs cannot finish in fifteen minutes
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-10 01:03:51 +02:00
mesh-admin a8b9be8d7f Merge pull request 'Issue 360: the operator cannot choose a notification's action on a touchpad' (#260) from issues/360-the-operator-cannot-choose-a-notifications-action-on-a-touchpad into main 2026-10-09 22:21:30 +00:00
jochen fe970e2f00 Issue 360: the operator cannot choose a notification's action on a touchpad
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The desk's actions sit on dunst's middle click, which a touchpad cannot give, and the context menu
names a command dunst's PATH does not hold. Located in the dunst module; dunst's own code settles
what a tap with do_action does.
2026-10-10 00:07:01 +02:00
mesh-admin c51aedea22 Merge pull request 'Issue 359: a secret given reached the operator as "needs a look", and nothing said why' (#259) from issues/359-a-secret-given-reached-the-operator-as-needs-a-look into main 2026-10-09 22:00:16 +00:00
jochen 163521f32e Issue 359: a secret given reached the operator as "needs a look", because its words held a clock time and the fallback was silent
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 23:40:57 +02:00
mesh-admin 32c0f6efb1 Merge pull request 'Issue 358: a mesh directory outside a home is changed by path, through any link; issue 356 points at it' (#258) from issues/358-a-mesh-directory-outside-a-home-is-changed-by-path into main 2026-10-09 18:08:04 +00:00
jochen e68d0ffa1d Issue 358: a mesh directory outside a home is changed by path, through any link; issue 356 points at it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The confirmation review of issue 356 found that the by-path chmod and chown of an ordinary mesh directory outside a home follows any link in its path. Issue 356 closed it for a handed-over directory, on every apply; the general gap is older and wider, and is recorded here rather than fixed in passing.
2026-10-09 19:59:06 +02:00
mesh-admin fbaa8e16e6 Merge pull request 'Issue 353: the controller asked before the bus held its grant' (#253) from issues/353-the-controller-asked-before-the-bus-held-its-grant into main 2026-10-09 17:57:38 +00:00
mesh-admin 0e7af9988a Merge pull request 'Issue 356: the operator's command ran a stale engine, and a hand-over had no channel; ADR 0272 gains the operator's rule and words.py the daemons' lines' (#256) from issues/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel into main 2026-10-09 17:57:18 +00:00
jochen 2cb779d06f Issue 354: quote the condition's old words, which the daemons' lines check refuses as a line to type
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 19:46:36 +02:00
jochen d1b42b7430 Merge remote-tracking branch 'origin/main' into issues/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check fail: its merge-check.sh failed: words: 2 problem(s)
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 19:46:03 +02:00
jochen 19776115ef Issue 356: the subject proves nothing, the signature does; the apply refuses a link at use
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
The review of 2026-10-09 proved a responder can reach the hand-over's subject through a reply, and that the apply followed a link the hand-over had refused. The record no longer says only the controller may publish there: it says the ask is signed with the mesh's key and verified by the engine, and that a handed-over directory is changed through a descriptor that followed no link. To-be 45's writers table gains the row for a hand-over asked of a machine.
2026-10-09 19:45:54 +02:00
jschoubben adbd57a1f9 Issue 353: the condition cleared at 18:11:44, about 15 minutes after the last refusal, not an hour after
mesh/repo-check pass: its merge-check.sh passed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery delivered
2026-10-09 19:35:09 +02:00
mesh-admin b962e87cae Merge pull request 'cycle.py: skip a rejected option by its ## section, so one under a ### subheading is skipped too' (#257) from fix/cycle-skips-rejected-options-by-their-section into main 2026-10-09 17:33:52 +00:00
jochen 189dbcef7a Skip a rejected option by its chapter, so one under a ### subheading is skipped too
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
cycle.py skipped a rejected option only when "Options rejected" was its nearest heading; a record that
lists its options under ### subheadings of "## Considered Options" had them held as deferrals.
2026-10-09 19:32:16 +02:00
mesh-admin 169200fe55 Merge pull request 'Issue 354: a decided rename was never scheduled, and a record that defers work names its issue' (#255) from issues/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live into main 2026-10-09 17:31:45 +00:00
jochen 2349f4bb63 Hold an insight to the deferrals it speaks about, and name what ADR 0273 left undecided
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
A review found the check too lenient: an issue cited as context beside a deferral passed it, and one
insight excused every deferral in a record. The tracking issue is now named as tracking it, an insight
covers only its section or what it quotes, and the phrases include "not decided here", "postponed" and
"future work". ADR 0273's four undecided items get issue 357.
2026-10-09 19:29:18 +02:00
jochen 85da2fb93f Issue 354: a decided rename was never scheduled, and a record that defers work names its issue
ADR 0244 left the code renames of the node-engine and the tool runner for later with no issue, owner or
date, and two days on the operator believed them live. Open the issue with the inventory, the risk and a
staged plan; add the rule to playbooks 02 and 04, as a dated insight on ADR 0244, and as a cycle.py check
that found the same gap in ADR 0270 (issue 355).
2026-10-09 19:27:05 +02:00
mesh-admin 1365ec17c8 Merge pull request 'ADR 0273: the agent's home is the module's, and the operator's personal rules and lessons are registered through it' (#254) from decision/0273-the-agents-home-configuration-is-the-modules into main 2026-10-09 17:26:55 +00:00
jochen a5433164c5 ADR 0273: act on drift at once, watch settings.local.json and plugins, and move unrendered files after adoption
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Review of hq #254: a hand edit is written back on the look that finds it, and a key that
decides what runs is never taken over; the guard and the condition cover settings.local.json,
plugins and plugin installs; after a node's first adoption an unrendered file is moved to the
copies; autoMode is a permission rule; the named leftovers, the owned and excluded entries,
pending requests with the exact nox line, and the agent's existing corrections as first lessons.
2026-10-09 19:24:35 +02:00
jochen 858e5413fa Issue 356: the operator's command ran a stale engine, and a hand-over had no channel
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The engine on every node's PATH was the one a person copied at installation, a build behind every delivery; and the way out of issue 339 was a terminal act at the machine, which ADR 0272 and the operator's rule of the same day make impossible to type. The record, located in the engine, the controller and the catalogue's check, with the fixes' pull requests; ADR 0272 gains the rule as a dated insight: the operator types one command, nox, and every text addressed to the operator names a nox line. words.py now refuses a daemon's line, with --list lines for the catalogue's copy; the lifecycle and the operator's-machine designs and issue 343's quotations are reworded.
2026-10-09 18:36:58 +02:00
jochen 2b1edf046d ADR 0273: the agent's home is the module's, and the operator's personal rules and lessons are registered through it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Research 040 measured the homes on four nodes: a skill retired a week ago on all four, the
operator's rules on one, nothing raised. The module now owns every configuration file under
the agent's home, excludes the program's working state by name, and raises agent-home-drift
on a hand edit; a fourth scope carries the operator's personal rules to every node; an agent
proposes a lesson when corrected and the operator accepts it at the terminal or on a warrant.
To-be 36 §10 and to-be 40 WP7–WP10 say what is built; ADR 0216 and 0243 gain notes.
2026-10-09 18:11:26 +02:00
jschoubben 2bceb5fbaa Issue 353: the controller asked before the bus held its grant
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 18:09:59 +02:00
mesh-admin c613cf0b82 Merge pull request 'Issue 352: a gate read another send's report, and a put-back chose a controller older than its store' (#252) from issues/352-a-gate-read-another-sends-report-and-a-put-back-chose-a-controller-older-than-its-store into main 2026-10-09 15:40:16 +00:00
jschoubben 903340516c Issue 352: a gate read another send's report, and a put-back chose a controller older than its store
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The 2026-10-09 16:19-16:36 sequence on the control node: a release's gate
held the machine's report against a newer send another plan made, failed
three healthy builds and put them back to a controller that could not
read the store; that controller then passed the newer plan's gate from
its own health. Measured and guessed apart; the fix in review.
2026-10-09 17:15:43 +02:00
mesh-admin 793e98d45a Merge pull request 'Issue 351: a held delivery whose module a backlog walk carried to every machine is never covered' (#251) from issues/351-a-held-delivery-whose-module-a-backlog-walk-carried-is-never-covered into main 2026-10-09 15:01:44 +00:00
jschoubben 261bee1cab Issue 351: a held delivery whose module a backlog walk carried to every machine is never covered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The catalogue's #125 stayed held 28 hours after ADR 0270 went live. The record covers a held merge when a
later delivered walk sent its module everywhere, as the machine steps say; a build held back for a person
and sent on the operator's word travels by a backlog walk, which gives one step per machine and whose end
mesh-delivery read for nothing. Report, diagnosis located in mesh-delivery, the fix in mesh-catalog #163,
and a progressive insight on ADR 0270's one-machine consequence, which this widens.
2026-10-09 16:53:11 +02:00
mesh-admin eec84794e3 Merge pull request 'ADR 0259: an ask is a message on the bus, any channel answers it, and the asker acts on a warrant' (#211) from decision/asks-answered-on-any-channel into main 2026-10-09 14:30:48 +00:00
jschoubben 3e84e4cb60 ADR 0259: accepted as revised on 2026-10-09
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 16:23:44 +02:00
jschoubben 32107428c4 ADR 0259: a rehearsal, not a drill; the announcement at the terminal checked; the operator's steps with nox, and the standard-input gap said
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 15:38:41 +02:00
jschoubben ec9bab5ee8 ADR 0259: name the confirmation review's N1 findings and their fixes — an early desk unlink bars nothing, a trusted party's secret is never given at a desk — and root-free answered by the serving controller alone, a silence never ending an approval, the link bound's residual risk 2026-10-09 15:36:36 +02:00
jschoubben 69c6ff73e6 ADR 0259: after the review of 2026-10-09 — root-free as a positive measure, warrants under ids nobody can guess, canonical digests, the code shown to a person or not at all, unlinking bound to the account and possible in the first hour, break-glass named, give as built; the operator's desktop client and the controller's write to the router's state stated as known gaps (issue 344) 2026-10-09 15:36:36 +02:00
jschoubben 8273faa4f0 ADR 0259: an ask the controller could not deliver is a condition of its own 2026-10-09 15:36:36 +02:00
jschoubben de5c89a211 ADR 0259: the drill as the live acceptance, another account's answer told to the operator, an ask's message never folded 2026-10-09 15:36:36 +02:00
jschoubben ba49876e48 ADR 0259: bind a warrant to the exact ask and act, count the login shell where execute is served (ADR 0268), fail closed where root is not measured, and keep what is trusted out of settings (issue 342) 2026-10-09 15:36:36 +02:00
jochen 7e158c739e Say how a module of its own account is served, and how the root probe is checked 2026-10-09 15:36:36 +02:00
jochen 7dd83d4435 Record the operator's choice for the control node, and the second way an agent reaches root there 2026-10-09 15:36:36 +02:00
jochen a04f1b90a0 Name the router, not the controller's retired verbs, where the glossary and to-be 46 still did 2026-10-09 15:36:36 +02:00
jochen c08b621491 Revise ADR 0259 after the security review: the router and verified channels get bus accounts of their own, verified-sender follows placement, and unlinking is asked of the linked account 2026-10-09 15:36:36 +02:00
jochen b63d27ded1 Keep an ask's tickets in its own record, where its closing ends them 2026-10-09 15:36:36 +02:00
jochen 1e51c062f6 Say which defects the channel fixes and which ask is refused, as built 2026-10-09 15:36:36 +02:00
jochen 1ebbf9007a Name the channel's failure and standing as intake events, and where the router reads each kind's capabilities 2026-10-09 15:36:36 +02:00
jochen 1714132a08 Accept ADR 0259 with the operator's two decisions, and amend to-be 46 to it 2026-10-09 15:36:36 +02:00
jochen 9f66452fff ADR 0259 (proposed): let any module ask a person and any channel answer, so Telegram is not built into the controller 2026-10-09 15:36:36 +02:00
mesh-admin 80b02dc9a0 Merge pull request 'Issue 348: a resolver that stopped answering ran on unnoticed, and builds sent after it were blamed' (#249) from issues/348-the-resolver-stopped-answering-and-nothing-restarted-it into main 2026-10-09 13:25:22 +00:00
jschoubben fbd00a16ca Issues 348 and 349: the review's answers — gaps, parts, D2 unchanged, the newest merge in any state
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 15:05:42 +02:00
jschoubben 17b64270b0 Issue 348: the manifest test takes either binding until the catalogue merges
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 14:22:17 +02:00
jschoubben 3acc1ec219 Issue 349: a merge acted on late superseded the merge after it, and built the older commit
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Plans of a branch superseded each other by when they were made; the
catch-up's late plan of an older merge took over a newer one, once a
security fix to the forge's module. Recorded with the fix in review.
2026-10-09 14:12:42 +02:00
jschoubben 582be67f05 Issue 348: a resolver that stopped answering ran on unnoticed, and builds sent after it were blamed
Records the 2026-10-09 outage on the control node: what was measured
(the unit ran, the private address refused; a restarted node-engine's
undecided statement cleared the network condition and its reopening
dated the fault after the send), what the reproduction showed of
bind-dynamic, what is still guessed, and the fixes in review.
2026-10-09 14:12:42 +02:00
mesh-admin fce1bbb432 Merge pull request 'ADR 0272: progressive insight — the terminal's mark, the login session, ordinary lines are reads and settings' (#248) from insight/0272-the-terminals-mark-and-the-login-session into main 2026-10-09 12:02:53 +00:00
mesh-admin 3b99661b27 Merge pull request 'Issue 344: the known gaps in the agent's boundary, accepted for now' (#244) from issues/344-known-gaps-in-the-agents-boundary into main 2026-10-09 11:59:23 +00:00
jochen 37bc302d7d ADR 0272's note: what the login-session rule does not do, and the class=user record
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 13:56:04 +02:00
jochen 57cfdb5cf9 Issue 344: gaps 12 and 13 closed by mesh-catalog #153 and mesh-controller #177
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 13:49:54 +02:00
jochen cd70eb7f6a ADR 0272: progressive insight — the terminal's mark, the login session, ordinary lines are reads and settings
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 13:42:17 +02:00
mesh-admin 6ea93e824a Merge pull request 'Issue 347: a pull request opened after its push is never checked, and nothing says so' (#247) from issues/347-a-pull-request-opened-after-its-push-is-never-checked into main 2026-10-09 11:38:40 +00:00
mesh-admin 54d974f178 Merge pull request 'ADR 0272 (accepted) and issue 343: the controller's terminal has no command; mesh-cli on every node' (#243) from decision/0272-mesh-cli-the-operators-command into main 2026-10-09 11:38:36 +00:00
jochen fc449617be Issue 347: a base requiring only mesh/repo-check counts, and the delivery's owner says it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The confirmation review: hq requires only mesh/repo-check, so a watcher
that looked for mesh/merge-gate alone would never have said this
repository's own unchecked pull request. And the delivery domain's own
condition, a delivery held past its bound, says it rather than a provider
standing.
2026-10-09 13:16:27 +02:00
jochen 4b5793144d ADR 0272: §4 as built after the confirmation review, ADR 0266 linked now it is merged
mesh/repo-check pass: its merge-check.sh passed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery delivered
An ordinary line meets the generic verb's reading forms and never a
terminal-only command (node account and node agent-account by ADR 0266,
token, operator, a secret's export); a terminal line drops ADR 0266's
served mark, so it reads as the terminal. How it is checked says the
withholding tests search base64 too and fail when what they guard goes,
and the control-node refusal is tested through the acts.
2026-10-09 13:15:59 +02:00
jochen 3bdd4e8040 Merge remote-tracking branch 'origin/main' into decision/0272-mesh-cli-the-operators-command 2026-10-09 13:15:36 +02:00
jochen d3cb9fedf4 ADR 0272 accepted (the operator, 2026-10-09)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
2026-10-09 13:02:45 +02:00
jochen d7c97d02fa Issue 344: name the node-engine's repository as code
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:56:54 +02:00
jochen bac7feca77 ADR 0272: the review's answers — every line a call, the socket grants nothing, the terminal does not follow assign, each claim and how it is checked
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
- §3: the node-engine vouches and decides nothing; deadline first, other
  namespaces refused, each account bounded and answered busy; the subject
  is the machine's own in the writers table.
- §4: phase 1 refuses the agent account (phase 2 answers it); an ordinary
  settings line takes the settings verb's path; the controller's module is
  assigned at the terminal only, so the terminal does not follow assign;
  'no wider than today' holds only while its two measured facts hold.
- §5: every line is a call, followed to its answer; 'nothing ran' only of
  a line nobody took, otherwise 'not known' with the call; the answer is
  its asker's alone.
- §7: after the four merge, the operator assigns mesh-cli to every node.
- How it is checked: each claim with its test or measurement, the
  MESH_VERB refusal in mesh-cli named a courtesy, and the sudo and
  agent-account facts read on the control-node today.
2026-10-09 12:56:44 +02:00
jochen 3143f5c698 Issue 344: gap 9 narrowed by mesh-host #61; the tools' sudo and the forge's rule precedence 2026-10-09 12:56:41 +02:00
mesh-admin 2700ef7096 Merge pull request 'ADR 0266 and issue 333: on the control node, agents run under an account of their own' (#225) from decision/0266-agents-run-under-an-account-of-their-own into main 2026-10-09 10:56:11 +00:00
jochen 2ba0d68306 Issue 347: a pull request opened after its push is never checked, and nothing says so
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The forge's announcer reads only repositories whose update time moved; a
pull request opened after its push does not move it. Located in the forge
module, with the fix (mesh-catalog) and how it is checked.
2026-10-09 12:55:26 +02:00
mesh-admin 6a7cf421a9 Merge pull request 'Issue 346: a setting a verb may set is pasted raw into its file, so an untrusted file is an injection point' (#246) from issues/346-a-setting-a-verb-may-set-is-pasted-raw-into-its-file into main 2026-10-09 10:51:03 +00:00
mesh-admin 2ed56a80b8 Merge pull request 'Issue 345: a calendar reminder shows transparent and empty, and the mesh had no tool to look at it' (#245) from issues/345-a-reminder-window-shows-transparent-and-empty into main 2026-10-09 10:50:55 +00:00
jschoubben 0ff6bd2267 Issue 346: user-dirs.dirs is trusted again for now (mesh-catalog #158), the stop-gap before the fix
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 12:40:26 +02:00
jschoubben 79f31ca113 Issue 346: a setting a verb may set is pasted raw into its file, so an untrusted file is an injection point
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Found in the review of the picom settings: a backend of xrender"; write-pid-path = "… adds an option of
the caller's. The two files marked trusted false on main are judged (user-dirs.dirs open, dunstrc not),
and typed settings validated at the verb and quoted at render are proposed; until then nothing new goes
trusted false.
2026-10-09 12:35:30 +02:00
jochen cb907d5810 ADR 0266: an agent can merge its own pull request, and the restart condition's quiet counts on the controller's clock
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The record said a merge needs a person; the forge requires no approval and
the merge tool merges once the checks pass, so nothing enforces the
operator's yes. What still remains now says so, and that the restart fix
counts from when the controller first saw the search pending. Item 10 and
routes A and B describe the follow-ups of the review of 2026-10-09.
2026-10-09 12:27:13 +02:00
jschoubben a4e132e47b Issue 345: picom is the first suspect, the graphics stack, and compositing off as the first test
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator reports picom leaving a chat window stale after a workspace switch before, never recorded.
The diagnosis now names the graphics stack, puts picom first among the candidates, and makes the first
step at the next empty reminder turning compositing off, which loses nothing; a Betterbird restart comes
after, refused while a message is being written unless the operator says yes.
2026-10-09 12:25:42 +02:00
jochen 11d261e582 Issue 344: the screen is readable through the display server's tools
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:19:54 +02:00
jochen 41060224e6 ADR 0272: the agent account's judgement comes from the engine's statement, never from the request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:13:03 +02:00
jochen e143f6b193 Issue 343: name the node-engine's repository as an identifier
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-09 12:12:34 +02:00
jochen 57bb4dab74 Issue 343: phase 1 of ADR 0272 is open as four pull requests 2026-10-09 12:12:23 +02:00
jochen 322f3aec36 ADR 0272: a path in a line is read where the controller runs
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:11:25 +02:00
jochen e8f0f43f1b Issue 344: an agent can merge its own pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:09:17 +02:00
jschoubben 599c3835f0 Issue 345: a calendar reminder shows transparent and empty, and the mesh had no tool to look at it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Records the symptom, what four reminders raised from a throw-away profile showed (drawn every time, at
depth 32), what that rules out, the candidates left, and what to measure at the next real reminder.
Status diagnosing: the cause is not known.
2026-10-09 12:07:49 +02:00
jochen 7d4678c5fb Issue 344: record the known gaps in the agent's boundary, accepted for now by the operator
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:03:36 +02:00
jochen 1ad9959b16 ADR 0272: say how the socket keeps ADR 0004, answer the operator and agent accounts only, and keep the bus request out of the SDK (ADR 0039)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 12:01:45 +02:00
jochen 95d78f7011 ADR 0272 (proposed) and issue 343: the controller's terminal has no command
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The records send the operator to "the controller's terminal", and on the
control-node `mesh-controller` is not a command: the controller is a mesh
process with its own account and environment, and nothing puts anything on
PATH. The record proposes mesh-cli, a module on every node whose program
asks the node-engine over a local socket; the node-engine vouches for the
account from the kernel and asks the controller on its node's own subject;
the controller runs the line as the terminal only for the operator's account
where no agent can be that account. The glossary gains mesh-cli, nox (its
zsh alias) and the controller's terminal.
2026-10-09 11:59:34 +02:00
jochen d7999e6fc9 ADR 0266: the setuid search's bound is 15 minutes, and the two routes and the restart condition the review of 2026-10-09 found
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Item 10 said two minutes; the engine's bound is 15, with a two-minute limit
inside it still to settle. What still remains now names a build from another
repository registering a module (mesh-controller 174), the forge's tools
changing its protection (mesh-catalog 153), and the urgent condition every
engine restart raises (mesh-controller 175).
2026-10-09 11:49:25 +02:00
jochen ee314a26ca ADR 0266: say where the boundary stands on 2026-10-09, and the rollout's order
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The routes found around the agent account since the record was written are
closed on main (the store's reader, places and served keys, what every session
obeys, execute withheld on the control node); the record names them, drops the
store gap it said kept the boundary from holding, and says plainly what still
remains, first the login shell's execute on the other nodes. To-be 36 §9 gains
the prerequisites and the order the parts go out in.
2026-10-09 11:03:27 +02:00
jochen e215dd95c4 ADR 0266: record the command allow list, the judge's final lists, and the store gap
The re-review closed the command verb to reads, moved enrolment to the
controller's terminal, widened what the node-engine judges, and found the
store's reader able to read every secret; say each, and that the boundary is
not in force until the store is fixed.
2026-10-09 10:15:47 +02:00
jochen dbc3ec40e0 ADR 0266: say it detects rather than prevents, and bound what review found
The review found ways round the record: a verb that set the accounts, root
following a link below a home, a tool reading any path as the operator, a
restore written beside the agent's files, a stale verdict passing. Record each
bound, the exact ways judged and not judged, the guard on every tool and its
record's limits, and a rollout order the guard does not block.
2026-10-09 10:15:47 +02:00
jochen 537e5b733b ADR 0266: on the control node agents run under an account of their own
The operator chose this remedy for ADR 0259 section 8: an agent there could
become root through the operator account's sudo and docker group, and read the
tool runner's credential. Record the node fact, the node-engine's judgement
the self-check reads, the guard, and how a session starts; issue 333 holds
the measured symptom and to-be 36 the design.
2026-10-09 10:15:47 +02:00
mesh-admin d81ae38970 Merge pull request 'Issue 340: an agent can plant a hook in every agent session on the mesh' (#240) from issues/340-an-agent-can-plant-a-hook-in-every-session into main 2026-10-09 08:00:25 +00:00
jochen cdf0e71d87 Issue 340: removing from or restoring to the operator's own home is refused too
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 09:51:57 +02:00
jochen 0badde3a56 Issue 340: say plainly which test refuses the new url
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 09:43:12 +02:00
jochen 523bb9c55a Issue 340: the home scope reached the operator's own home, and bus text still renders
The confirmation review found the home scope wrote the operator's own
~/.claude on nodes where the agent is the operator, and that text put
straight on the bus at the mesh or node scope still renders; record both,
the fix for the first, and what closes the second.
2026-10-09 09:43:03 +02:00
mesh-admin b43505d88b Merge pull request 'ADR 0270 and 0271 (accepted): a held delivery a later merge contains, or that moves nothing, ends by itself; a send held for the bus's step is told at once' (#241) from decision/0270-0271-held-deliveries-end-and-bus-step-said into main 2026-10-09 07:35:21 +00:00
mesh-admin 29bf2cae30 Merge branch 'main' into decision/0270-0271-held-deliveries-end-and-bus-step-said
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-09 07:30:09 +00:00
jochen 8a6f19e457 Issue 340: record the operator's decisions, and open 342 for passwords taken as settings
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Shared agent text is the terminal's until a surface of the operator's own
updates it, the wider controller rule stands, and a password is not a
setting: 342 names every module that takes one through a setting.
2026-10-09 09:29:39 +02:00
jochen cbd9f092d8 ADR 0270 and 0271 accepted: their home in designs 47 and 45, and the glossary's final states
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator accepted both on 2026-10-09. Design 47's state table gains covered and what ends a held
delivery by itself, and its group the wait for a composed check and the lift; design 45's signals table
gains S17 and its planned bus step says the wait is told at once. The glossary's delivery entry names its
six final states.
2026-10-09 09:24:30 +02:00
mesh-admin 9744ed7b9d Merge pull request 'Issue 336: a send held for the bus's planned step raises no condition, so the operator is never asked' (#228) from issues/336-a-send-held-for-the-bus-step-raises-nothing into main 2026-10-09 07:23:41 +00:00
mesh-admin a5656b9cce Merge pull request 'Record hygiene: resolve issues 310, 312, 313, 314, 318, 325, 327, 330 and 337, whose fixes are merged and live' (#239) from issues/hygiene-resolve-merged-fixes into main 2026-10-09 07:23:31 +00:00
jochen 79de22423d ADR 0270: how a failed new-module build is said, a one-machine module is never covered, and which gate tests the forge module
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Decision 1 named S6 for a failed new-module build; S6 is a lost build ask. Such a build is said by one
line in the controller's log until its module is assigned. A module on one machine never gets a 'rest'
step, so covered never reaches it. The forge module's tests run in the catalogue's TypeScript check, #151.
2026-10-09 03:24:10 +02:00
jochen 3d71532a3a Issue 341: a merge check judges the pull request's head, not what merging it makes
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Node-engine #66 passed both required statuses after #64/#65 had merged,
and its merge did not compile: each side added Applied.Declared. The
check clones the head alone and nothing marks a verdict stale when the
trunk moves; ADR 0237/0238 say the head, so the fix needs a decision.
2026-10-09 03:13:40 +02:00
jochen b04498c01f ADR 0270: the forge's ancestry decides covered, and say what covered means and does not (review of #241)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Merge order on a protected trunk is kept only as the first filter; the forge's compare, through a new
read-only tool, decides whether a later merge contains a held one. A merge held without a passing check is
covered only by a later delivery whose own check passed. Covered says a later merge that contains it was
delivered, not that the change runs: a revert in between stays possible. A delivery that only adds a module
is delivered whatever its build does, and covered wins over go when a person's walk fails between two
readings. The glossary gains covered beside superseded and delivered.

ADR 0271 names the bus upgrade as the mesh call and says S17 clears once the step sent the new bus.
2026-10-09 02:58:36 +02:00
jochen 1dbfb2d85a Issue 340: the whole layer of a module with a mergeable file is the terminal's, and the text still registrable is a risk said
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The review found that a mergeable file takes any key, not only those it
names, and that the text a session reads stays registrable through a verb;
say both, and the follow-ups the module took.
2026-10-09 02:55:23 +02:00
jochen 31e78d62d4 Name the pull requests that build ADR 0270 and 0271
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 02:35:05 +02:00
jochen 6163a74bb2 ADR 0270 and 0271 (proposed): a held delivery whose change runs ends by itself; a send held for the bus's step is told at once
Issues 319 and 316: fifteen held deliveries asked a person a question with no right answer, and stop,
the only way out, records a delivered change as an error. ADR 0270 delivers a merge that moves nothing at
once, adds the final state covered for a merge a later delivered walk of the same trunk carried, and lifts
a group hold once its composed check passed.

Issue 336: a send held for the bus's planned step raised nothing for 28 minutes. ADR 0271 adds row S17,
said at the first refusal for the operator with the verb, keeps S3 from calling it lateness, and has the
delivery plan say before the merge that a person's bus upgrade follows.

Each issue's diagnosis says what the fix does and how to verify it once delivered.
2026-10-09 02:34:18 +02:00
jochen 9bd0dcc611 Merge remote-tracking branch 'origin/issues/336-a-send-held-for-the-bus-step-raises-nothing' into decision/0270-0271-held-deliveries-end-and-bus-step-said 2026-10-09 02:31:05 +02:00
jochen 2953b8a8f4 Issue 340: an agent can plant a hook in every agent session on the mesh
mesh/repo-check pass: its merge-check.sh passed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery superseded: a newer head of the same pull request
Record the hole the review traced, why the module cannot tell who called,
which kinds run or are obeyed in a session and which stay registrable,
the fix in the controller and the module, what is registered today, and
that it must land before ADR 0266's agent account is switched on.
2026-10-09 02:30:11 +02:00
mesh-admin 171ac2a163 Merge pull request 'ADR 0269: what the mesh's own declaration stopped is started again when it goes' (#238) from decision/0269-what-the-meshs-own-declaration-stopped-is-started-again into main 2026-10-09 00:20:32 +00:00
jochen 120cdcd25a Issues 310, 312, 313, 314, 318, 325, 327, 330, 337: resolved, their fixes merged and seen live
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Each fix's pull requests are merged and delivered; each issue now says
what was read live on 2026-10-09 to show it, so the record stops
listing fixed faults as located.
2026-10-09 02:19:09 +02:00
jochen a11b14f7ae ADR 0269 accepted: note the narrowing on ADR 0118, and give it a home in the node-engine's design
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator accepted ADR 0269 on 2026-10-09. ADR 0118 gets the dated note the README asks for where a
later record narrows its mechanism. The node-engine's to-be design names the decision and says what it
changes, which the cycle check requires of an accepted record.
2026-10-09 02:17:32 +02:00
jochen 1a813ba313 ADR 0269: the -- check covers the calls in internal/system that name a service resource's unit
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 02:03:32 +02:00
mesh-admin 4b6642de5e Merge pull request 'Issue 339: the merge order against mesh-host #61, used-as-found as a behaviour change, the review's follow-ups' (#236) from issues/339-review-follow-ups into main 2026-10-08 23:55:39 +00:00
jochen 61ab74c70e ADR 0269: the catch-up happens when the timer's last run is more than a week back
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 01:54:51 +02:00
jochen 2e2be955e9 ADR 0269: say that a unit stopped by hand while held cannot be told apart, that a restored Persistent timer catches up at once, and what is left alone
Third review: a person's stopping a unit the mesh also held stopped leaves no trace, so it is started
again on removal; the home server's restored weekly timer would start a scrub of about 46 hours at once.
A masked or since-changed unit is left alone, and a failed restore waits.
2026-10-09 01:54:42 +02:00
jochen 68018c7c1b ADR 0269 (proposed): what the mesh's own declaration stopped is started again when it goes
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Unassigning the zfs module would leave the home server's pool with no scrub schedule: ADR 0118 never
starts a unit on the way out, so the distribution's timer the module held stopped would stay stopped.
The host now records what it declared beside what it found, and gives back only what the mesh's own
declaration stopped.
2026-10-09 01:45:40 +02:00
jochen 1d9d535912 Issue 339: an unmarked file counts as trusted, and the rollback hazard of the trusted field and its fix
mesh/repo-check pass: its merge-check.sh passed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery delivered
2026-10-09 01:42:51 +02:00
mesh-admin 5b0b550157 Merge pull request 'Issue 319: why no walk opened, and four held deliveries closed with stop' (#237) from issues/319-diagnosis-held-deliveries-closed into main 2026-10-08 23:39:02 +00:00
jochen a93ecf90ea Issue 319: record why no walk opened, and the four held deliveries closed with stop
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
A new module counts as something to move, so its delivery is held; a later walk
reaches only the delivery whose merge is its commit. Four covered deliveries were
stopped before this issue was found, and their merged pull requests now say error.
2026-10-09 01:38:02 +02:00
jochen 5d054b722d Issue 339: the terminal's settings are derived from what a module serves and which files it trusts; a found directory is its own condition the gate never reads as a fault
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 01:24:14 +02:00
jochen 5fbd89c062 Issue 339: the gate reads a directory used as found as a wait for a person; the trust anchors are the terminal's
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 00:55:54 +02:00
mesh-admin e62ab24658 Merge pull request 'ADR 0268: the login shell's execute is withheld on the control-node until a call to it needs a person's approval' (#235) from decision/0268-the-login-shells-execute-is-withheld-on-the-control-node into main 2026-10-08 22:30:08 +00:00
mesh-admin ef2a38474f Merge pull request 'Research 039: a tool or verb that needs a person's approval' (#233) from research/039-a-tool-that-needs-a-persons-approval into main 2026-10-08 22:30:03 +00:00
jochen ab027691e1 Issue 339: name the merge order against mesh-host #61, record used-as-found as a behaviour change, and the review's follow-ups
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 00:28:55 +02:00
mesh-admin 0f02ac47d4 Merge pull request 'Issue 339: any caller of the settings verb could have the node-engine hand a system directory to an account' (#234) from issues/339-a-verb-can-place-a-directory-at-the-machines-own into main 2026-10-08 22:21:03 +00:00
jochen 7bd868786f ADR 0268: the login shell's execute is withheld on the control-node until a call to it needs a person's approval
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator's decision of 2026-10-09: execute runs any command as an
account that can become root without a person, so the control-node stops
offering it now, and every node marks it as needing approval once the
mark exists. To-be 41 gains WP7 for the seat and the shell module.
2026-10-09 00:12:53 +02:00
jochen ccb54450ea Research 039: the login shell's execute is the first case, and narrow read tools must come first or approvals become noise
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-09 00:01:47 +02:00
jochen ac3a67ebab Issue 339: any caller of the settings verb could have the node-engine hand a system directory to an account
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
Located in the controller (places and accesses taken through every verb, no line-break check) and the node-engine (a found directory chowned as root, through a link too, and any path mounted). ADR 0266's boundary depends on the fix.
2026-10-09 00:00:04 +02:00
jochen c31a76cac5 Research 039: a tool or verb that needs a person's approval, so an agent may ask but cannot act alone
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 23:59:45 +02:00
mesh-admin e2de8811a3 Merge pull request 'ADR 0267: an artifact is rebuilt only when its build source changes, and a module moves only along build dependencies on artifacts' (#232) from decision/0267-a-module-moves-when-its-build-source-changes into main 2026-10-08 21:13:14 +00:00
jochen d2fe9bc870 Accept ADR 0267: the operator's word of 2026-10-08
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
Set its status to accepted and state the notes on ADR 0162, 0238 and 0236
as decided.
2026-10-08 23:11:35 +02:00
jochen 7d3f4407a7 ADR 0267 (proposed): an artifact is rebuilt only when its build source changes
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
A merge to a shared repository moved every module that read it, in a gated
tier each (issue 338). The operator's rule maps a change onto the artifacts
whose build source it touches and adds only modules with a build dependency
on a changed artifact; sharing a repository is no dependency. Supersedes in
part ADR 0162 decision 1 (the packages kind) and ADR 0238 decision 3 (whole
repository and context reads); adds build source to the glossary.
2026-10-08 23:05:51 +02:00
mesh-admin d0be529194 Merge pull request 'Issue 337: the store's reader can read the controller's signing key (critical)' (#230) from issues/337-the-stores-reader-can-read-the-controllers-signing-key into main 2026-10-08 20:16:30 +00:00
jochen 7f78016f07 Issue 337: say that only what is gone is reported revoked, and that a grant the reader passed on is taken back with CASCADE
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 22:11:16 +02:00
jochen b17839a7db Issue 337 after review: say control-node, drop the store's operational settings, and match the fix's revoke-by-grantor and read-back
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
Adds what the fix cannot see (definer functions and views; the name check
as a backstop) and, as a follow-up, that the reader's read-only session is a
default a statement can turn off, so the missing write grants are the guard.
2026-10-08 22:02:30 +02:00
jochen 7ade322bcb Register issue 338: a module built from a shared repository moves on every merge to it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
A controller merge walks three gated tiers because the route proxy and the
build seat are planned by repository, not by the paths their builds read,
and their programs carry the commit. Located; fix directions recorded.
2026-10-08 21:59:18 +02:00
mesh-admin 4955a663d3 Merge pull request 'Issue 335: a build that passed its gate was failed and put back by the first machine's later reports' (#227) from issues/335-a-passed-gate-was-failed-by-later-reports into main 2026-10-08 19:51:27 +00:00
jochen bbbaf943e8 Issue 335: resolved by mesh-controller#165 (8170fc5) with replay R335
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 21:49:00 +02:00
jochen 6c297214ad Issue 337: the store's reader could read the controller's signing key and every secret the store holds
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Opened and diagnosed: pg_read_all_data on the login the store seat's query
runs as. Says what the store can and cannot tell about whether the key was
read, why it should be rotated once the fix is taken, and that no rotation
mechanism exists yet. The fix is mesh-catalog PR 143.
2026-10-08 21:46:01 +02:00
mesh-admin a1c85c6de5 Merge pull request 'Design 52: until it is adopted, the workstation's fstab line still mounts at boot (review follow-up)' (#229) from fix/shares-review-followups into main 2026-10-08 19:37:03 +00:00
mesh-admin ebffbaa17e Merge pull request 'ADR 0264 and issue 330: a message a consumer gives up on is kept until a person delivers it again or drops it' (#223) from fix/330-a-message-given-up-on-is-kept into main 2026-10-08 19:26:57 +00:00
jochen 4dd52cace6 Design 52: say the workstation's fstab line still mounts at boot until it is adopted, and can hold the boot 30 s
mesh/delivery-group group fix/shares-review-followups delivered: every member is delivered
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-08 21:14:34 +02:00
jochen 6fdaeb79ba Issue 334 and ADR 0264: an ask a seat gave up on can only be dropped for now
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
The review refused a grant over every seat's queue, and a copy published
again left the original stuck in it.
2026-10-08 21:08:12 +02:00
jochen b0986e2ae0 Issue 330: name the installer's user list among its fixes 2026-10-08 21:08:12 +02:00
jochen d1e0e121d6 ADR 0264 and issue 330: keep what a consumer gives up on until a person acts
Design 25 promised a dead-letter stream nothing built; a message a consumer
gave up on was lost after a week, silently once its condition cleared.
2026-10-08 21:08:12 +02:00
jochen e6f0e7e359 Issue 336: a send held for the bus's planned step raises no condition, so the operator is never asked
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
For 28 minutes on 2026-10-08 every send to the control-node waited for a bus upgrade only a person can start, and nothing but a walk's note said so. Located in the controller: the refusal is kept as the walk's note, S3 fires only past a 30-minute bound with generic words, and probe DB reads only a running step.
2026-10-08 20:58:44 +02:00
jochen f346efb938 Issue 335: leave fixed-by for the merge, and call the put-back refusal a guard
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-08 20:49:40 +02:00
mesh-admin 4eb8be9e66 Merge pull request 'ADR 0263 (accepted), research 038, design 52: a share is a provision, a node mounts it on demand' (#221) from feat/mounts-module into main 2026-10-08 18:39:59 +00:00
jochen b409537dbc Issue 335: a build that passed its gate was failed and put back by the first machine's later reports
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Recorded so the gate's verdict is known to be final, and the replay R335
holds it.
2026-10-08 20:27:36 +02:00
jochen d05c9d7ca4 Name the safe order of taking shares into use without the retired word
mesh/delivery-group group feat/mounts-module delivered: every member is delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 20:22:05 +02:00
jochen bffd0f90d0 ADR 0263 and design 52 after review: what each export promises, per-node grants, refusals, not-built checks, and a read-only-first rollout 2026-10-08 20:21:48 +02:00
mesh-admin 0811b21be1 Merge pull request 'ADR 0265 and issue 327: a verb that reads is answered on the serving controller's own connection, and every connection says what it is' (#224) from fix/327-a-verb-reads-on-the-serving-connection into main 2026-10-08 18:21:28 +00:00
jochen 0a38d9b21d ADR 0265: a panicking verb answers, and refused logins are not a reconnect loop
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/327-a-verb-reads-on-the-serving-connection delivered: every member is delivered
mesh/delivery delivered
2026-10-08 20:04:19 +02:00
jochen c3a0f7a595 ADR 0265 and issue 327: answer read verbs on the serving connection and name every connection
Hundreds of short connections an hour, all named mesh-controller, hid any
client reconnecting in a loop.
2026-10-08 20:04:19 +02:00
mesh-admin 8665027873 Merge pull request 'ADR 0262: a left-out module's data is backed up as this controller reads it, and an unplaceable line is listed' (#226) from fix/0262-backup-as-read into main 2026-10-08 16:41:31 +00:00
jochen f30b2a3ae0 ADR 0262: note how a left-out module's data is backed up, as the controller now does it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The record said its data is still copied; the shipped follow-up backs it up as this controller reads
it and lists an unplaceable line rather than failing the machine.
2026-10-08 18:38:09 +02:00
jochen f4b79ba7c2 Design 52 says how the modules were built: the server's bucket carries each share's path, two root processes, settings default to none
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/mounts-module checking: 2 of 3 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 18:37:15 +02:00
jochen 34899385ec Accept ADR 0263 with the operator's choices: one share per folder so backups are never exported, mesh machines only 2026-10-08 18:23:23 +02:00
mesh-admin 79bd9911e6 Merge pull request 'ADR 0262: a preference has a default in its manifest, and the operator's own value has none' (#216) from feat/setting-defaults into main 2026-10-08 16:18:53 +00:00
jochen 6a9bc3f4b3 ADR 0262: say what a left-out module loses, and how far an entry's unknown key is read
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The record claimed a left-out module kept what the machine holds for it and that an entry's unknown key
was read like a top-level one; neither is true of the merged controller.
2026-10-08 17:55:38 +02:00
jochen f09c9befdf ADR 0262: a module with a field the controller does not know is left out and raised, never read silently
Dropping an unknown key would run a module without something its manifest says. Also name the check by
what a key is about, narrow the listing to one machine, and say how to roll back below this record.
2026-10-08 17:55:04 +02:00
jochen de7aca4fa6 ADR 0262: one listing of every preference, and research 037 surveying the literals to convert
The operator asked for one place that lists every module's preferences and each machine's value, and
for a list of the values the catalogues hard-code that should become preferences.
2026-10-08 17:55:04 +02:00
jochen 0e5a5e2085 ADR 0262: record the lenient read of stored manifests and the marked defaults layer
A stored manifest read strictly would fail the whole catalogue after a controller rollback, and a node
called default would lose its layer. Also scope the default, retire tunable, and list the new checks.
2026-10-08 17:55:04 +02:00
jochen 417db4d837 ADR 0262: let a preference carry a default so a running module can gain a setting
A ${setting:} nothing set failed composition, and settings refused a key no file asked
for yet, so no order of changes could add a setting to a running module. A manifest may
now default a preference; the operator's own value still has none.
2026-10-08 17:55:04 +02:00
mesh-admin acc82aa5e4 Merge pull request 'Issue 332: the service manager could neither clear a failed unit nor say what wants it' (#222) from issues/332-the-service-manager-cannot-clear-or-explain-a-failed-unit into main 2026-10-08 15:52:54 +00:00
jochen fd6c452e61 Issue 332: add unlink-dangling and the glob refusal from the review
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
disable cannot remove a link whose unit file is gone, so the verb that
finds it needed one that removes it.
2026-10-08 17:40:00 +02:00
jochen 3edafd78ab Research 038, ADR 0263 and to-be 52: let the mesh own shares and mounts, so no node depends on a hand-written export or fstab line
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/mounts-module rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 17:23:52 +02:00
jochen c3a20baece Issue 332: the service manager could neither clear a failed unit nor say what wants it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
A leftover user unit stayed raised on the workstation: no seat verb showed
the dangling enable link that keeps asking for it, or reset its failed
record, so the only step left was a shell. Lands per ADR 0246.
2026-10-08 17:21:16 +02:00
mesh-admin 9fc09fe0d5 Merge pull request 'Issue 331: resolved, the sensors condition cleared live' (#220) from issue/331-resolved into main 2026-10-08 15:17:28 +00:00
mesh-admin c9b0e55a1f Merge pull request 'Issue 331: fixed-by and replay R331' (#219) from issue/331-fixed-by-and-replay into main 2026-10-08 15:17:25 +00:00
jochen 7d60650ae7 Issue 331: resolve it, now the sensors condition is seen to clear on the laptop
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 17:12:50 +02:00
jochen a97efa99f8 Issue 331: name the pull requests that fix it and its replay R331
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Status stays located until the sensors condition on the laptop is seen to clear.
2026-10-08 17:11:33 +02:00
mesh-admin 72e0a851cf Merge pull request 'Issue 331: a declared tool check is never asked on the live link' (#218) from issue/331-a-tool-check-is-never-asked into main 2026-10-08 15:06:55 +00:00
jochen 33b0ee3d53 Issue 331: state how the fix is proved, what CI still skips, and that the grants hold live
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 17:02:52 +02:00
mesh-admin 31664fcd80 Merge pull request 'Issue 325 and ADR 0261: an assignment made while its module's build runs is kept pending' (#214) from fix/325-assign-says-why-a-module-is-not-there into main 2026-10-08 14:58:04 +00:00
jochen 6dda1b1d77 ADR 0261: one condition per pending assignment, and a raised row kept until it clears
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The second review of mesh-controller#150 changed points 1, 6 and 7; say
what the code now does.
2026-10-08 16:46:42 +02:00
jochen 3a09387c8e ADR 0261: an assignment made while its module's build runs is kept pending
The review of mesh-controller#150 found the controller now makes an
assignment after the person's act, which no record decided. Decide it,
cite it from the glossary and to-be 45, and correct issue 325's account
of what is settled where and what is sent.
2026-10-08 16:46:40 +02:00
jochen 63754ca874 Issue 325: assign said no module of that name while the module's build ran
Register the symptom and the fix in mesh-controller#150 (replay R325 in
mesh-lab#68), and name the pending assignment in the glossary.
2026-10-08 16:46:40 +02:00
jochen 08e065fbb6 Issue 331: record why no declared tool check was ever asked on a live machine
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 16:45:21 +02:00
mesh-admin 1782f1ad74 Merge pull request 'Issues 326-330: research 036's live bus faults, measured again and diagnosed' (#217) from issues/036-bus-faults into main 2026-10-08 14:29:17 +00:00
jochen c64a7b3d4a Issues 326-330: register research 036's live bus faults, each diagnosed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The bus's own numbers were unexplained and raised by nothing. Measured again
read-only: mesh-delivery rewrites unchanged deliveries (326), every controller
verb dials its own connection (327), API errors have no reader (328), event
consumers outlive their declaration (329), and design 25's dead-letter stream
was never built (330). The two slow consumers join issue 270, whose missing
system account they share.
2026-10-08 16:27:34 +02:00
mesh-admin 7d1e3822bf Merge pull request 'ADR 0260: three standing reads, drop-in directories are no module's, the lists grow only in the open' (#215) from fix/0260-path-check-facts into main 2026-10-08 14:27:19 +00:00
jochen e182e9ab12 ADR 0260: correct the count of standing reads, and say drop-in directories are no module's and the lists grow only in the open
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 16:12:54 +02:00
mesh-admin 91272a8e2c Merge pull request 'Research 036: everything moves over NATS — where the mesh still talks around the bus, and what a view onto the bus needs' (#213) from research/036-everything-over-nats into main 2026-10-08 14:12:02 +00:00
mesh-admin 9efa785f38 Merge pull request 'ADR 0260: a bar block shows a value its module keeps on the bus, and no module reads another's files' (#212) from feat/the-bar-takes-blocks into main 2026-10-08 14:09:26 +00:00
mesh-admin a95e8060c3 Merge pull request 'ADR 0257, issues 321-323, research 035: record the bases builds copy; what the pre-mesh images are' (#207) from decision/a-copied-base-is-recorded into main 2026-10-08 14:02:47 +00:00
mesh-admin dd316afa57 Merge pull request 'ADR 0254: correct what the fix covers after its review (progressive insight, issue 318)' (#210) from insight/0254-every-module-of-a-failed-send-gets-a-verdict into main 2026-10-08 14:02:21 +00:00
jochen 9afd6f8902 ADR 0260: say what review narrowed — one key per grant, per-machine state, wider path roots, a follower that speaks
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 15:54:45 +02:00
jochen 56e78612dc ADR 0257: the sweep after a build leaves an index for a confirmed collect
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Let go of alone, an index's platforms would stay for ever under a collected record
(re-review of #207).
2026-10-08 15:50:36 +02:00
jochen b7624cf023 ADR 0254 insight: state the two remaining costs, and point decisions 1 and 5 at their corrections
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 15:45:34 +02:00
jochen 5e9a27a6a3 Research 036: measure where the mesh talks around the bus, so views can be built from it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 15:28:57 +02:00
jochen 9b3ccfc1b4 ADR 0254 insight: a broken module goes back at once, and only a move that adds a group is excused, at a stated cost
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 15:28:31 +02:00
jochen 64ef5a0ac8 ADR 0254: correct what the fix covers after its review, as a progressive insight (issue 318) 2026-10-08 15:28:31 +02:00
jochen 427b078093 ADR 0260: a bar block shows a value its module keeps on the bus, and no module reads another's files
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator restated that modules talk over the bus; ADR 0255 had the bar print the
power module's file.
2026-10-08 15:24:04 +02:00
mesh-admin 4196b711f8 Merge pull request 'ADR 0256: one module reads a machine's hardware condition, and a fault is said through its health' (#206) from decision/hardware-sensors into main 2026-10-08 13:20:58 +00:00
mesh-admin f08342ad50 Merge pull request 'ADR 0258: name the mesh MCP server, not the retired console' (#209) from fix/needs-you-from-the-console into main 2026-10-08 13:20:45 +00:00
jochen df41dfff38 ADR 0256: say exactly how a SAS drive, a USB drive and a stuck read are treated
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The SAS sentence now holds for modes with spaces, a hung read is unhealthy in two to three minutes, not
one, every USB drive is told sat, and the limits of a stuck read are recorded.
2026-10-08 15:04:03 +02:00
jochen 39585e3bb2 ADR 0258: name the mesh MCP server, not the retired console
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/needs-you-from-the-console delivered: every member is delivered
The glossary retires "console"; the words an operator reads say where to act in
the mesh's own word.
2026-10-08 15:03:41 +02:00
mesh-admin f5f4eedf7a Merge pull request 'ADR 0254: a wait for a person's new login is not a gate's failure, and each module of a send keeps its own pass (issue 318)' (#204) from fix/318-a-wait-for-a-person-is-not-a-failure into main 2026-10-08 13:03:12 +00:00
mesh-admin fc9df13497 Merge pull request 'Issue 324 and ADR 0258: a desk click performs only an acknowledgement, an answer is a one-time ticket' (#208) from fix/notifications-after-review into main 2026-10-08 13:03:00 +00:00
jochen 0406373545 ADR 0256: one reader of the drives, and a stuck read is said
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
After the second review: smartd is dropped as a second reader that could wake or keep awake the drives,
a stuck read or a drive not looked at is unhealthy, a drive asleep for a day is HIGH and never unhealthy,
and a board chip's maximum says nothing of a fan. The never-woken and hourly claims now say exactly what
holds.
2026-10-08 14:59:12 +02:00
jochen 3bfedcfa15 ADR 0258 and issue 324: the live window, data loss never silenced from a click, and where to give what no notification can
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/notifications-after-review delivered: every member is delivered
The second review found the fix could not read what the live build kept, missed
quiet warnings known from history, and let a click silence data loss.
2026-10-08 14:56:48 +02:00
jochen b6690c38cf ADR 0257: platforms go only in a confirmed collect; say what stays, and what a rollback costs
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The sweep after a build is real collection with no dry run in front of it, so it lets an
index go alone (review of #207).
2026-10-08 14:53:04 +02:00
mesh-admin ef8b76461e Merge pull request 'ADR 0255: the bar takes blocks as data from the module that knows the hardware' (#205) from feat/the-bar-takes-blocks into main 2026-10-08 12:44:40 +00:00
jochen f5d08f4b6f Issue 324: name the pull requests that fix it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review checking: 2 of 3 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 14:40:30 +02:00
jochen dd149a72c0 Issue 324 and ADR 0258: a desk click performs only an acknowledgement
Review found that ADR 0253 let any process at the operator's session release,
stop or start a delivery in the operator's name, and that its tokens, fallback
words and quiet warnings failed on the paths that go wrong.
2026-10-08 14:40:02 +02:00
jochen 01e48196f7 Narrow ADR 0254 to what the walk path does, and say what a pending relogin excuses (issue 318)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/318-a-wait-for-a-person-is-not-a-failure delivered: every member is delivered
2026-10-08 14:38:08 +02:00
jochen e4299b5521 ADR 0255: say what review changed — if-capability on offered kinds only, unrenderable pieces left out and named, and why the sampler runs everywhere
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/the-bar-takes-blocks delivered: every member is delivered
2026-10-08 14:35:56 +02:00
jochen f63828ce1c ADR 0256: say exactly what raises a condition, and why a drive is read only hourly
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Rule 4 now matches the module after review: the NVMe temperature bit, old-age attributes, a fan's minimum
or alarm and a board chip's limits raise nothing; a stale watch does. The standby caveat is recorded: no
mesh tool sees a drive's spin-down timer, so spinning drives are read hourly.
2026-10-08 14:35:21 +02:00
jochen 781681a117 ADR 0254: a wait for a person's new login passes the gate, so one module's wait stalls no walk (issue 318)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/318-a-wait-for-a-person-is-not-a-failure checking: 3 of 4 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 14:25:40 +02:00
mesh-admin 1c987e0df7 Merge pull request 'Issue 320 and ADR 0253: a condition says itself to the operator in plain words' (#203) from feat/plain-notifications into main 2026-10-08 12:20:39 +00:00
jochen 59bc551b5b ADR 0257, issues 321-323, research 035: record the bases builds copy, and say what the pre-mesh images are
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The store's tools counted 23.8 GiB nothing records: copied bases the sweep could never
let go of, platforms a let-go index left behind, and images from before the mesh. Package
retention read no lockfile, so it kept versions by age alone.
2026-10-08 14:08:45 +02:00
jochen 356c5f23f4 ADR 0253: say whether the operator is needed, keep quiet what needs nothing, answer from the notification
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/plain-notifications delivered: every member is delivered
The operator read the reworded popup and could not tell whether to act, and was
sent to open a session to act at all. The record now decides the verdict, the
quiet rule and the path by which a notification is answered.
2026-10-08 14:00:38 +02:00
jochen c8b6a4eea8 ADR 0256: one module reads the machine's hardware, so a failing drive or fan is raised, not found dead
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
A machine's temperatures, fans and drive health were read by the laptop's vendor module only; the
machines' 20 drives were watched by nothing. The sensors module reads them on every machine, holds no
seat until a caller must reach it by role, and says a hardware fault through ADR 0240's health until the
machine judges its own hardware. To-be 48 gains §11 and phase G.
2026-10-08 13:48:52 +02:00
jochen 5852b66988 ADR 0255: the bar takes blocks as data from the module that knows the hardware
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/the-bar-takes-blocks rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
The operator chose a generic boundary for the bar: a block is said in terms no bar
owns and rendered by the seat's holder, placed only where the machine reports the
hardware, and generic hardware comes from the generic seat's module, not a vendor's.
To-be 42 records it; ADR 0208 points to it.
2026-10-08 13:45:27 +02:00
jochen fc3c42318f Issue 320: name the pull requests that fix it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/plain-notifications ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 13:29:33 +02:00
jochen 7c49aa0ffc Issue 320 and ADR 0253: a condition says itself to the operator in plain words
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator could not read the mesh's notifications: they were the summary meant
for an agent, with ids, commits, keys and verbs, in UTC. The condition now carries
a headline, an explanation and a resolved line beside its summary.
2026-10-08 13:28:50 +02:00
mesh-admin 73fa97efde Merge pull request 'Issues 316-319: holds that never lift, holds walked past, and one module's wait stalling every walk' (#202) from issues/316-319-holds-and-gates into main 2026-10-08 11:27:49 +00:00
jochen aa221abfee Issues 316-319: holds that never lift, a hold a later merge walks past, and one module's wait stalling every walk
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Four delivery defects seen on 2026-10-08, each located, so their decisions can be made before code moves.
2026-10-08 13:23:49 +02:00
mesh-admin df0097245d Merge pull request 'Issue 314: a large answer was lost on its way back; issue 200 resolved by 265's fix' (#198) from issues/314-a-large-answer-was-lost-on-its-way-back into main 2026-10-08 11:06:34 +00:00
mesh-admin c3022d5e14 Merge pull request 'ADR 0251 and to-be 51: the registries say what they hold and keep what is named' (#200) from design/051-the-registries-keep-what-is-named into main 2026-10-08 11:06:17 +00:00
mesh-admin 7879ade66f Merge pull request 'ADR 0252: a module puts an account in a group, and the mesh says when a new login is needed' (#201) from decision/0252-a-module-puts-an-account-in-a-group into main 2026-10-08 11:05:12 +00:00
jochen f91735b629 Put the store's tools in a module beside it, since the store's module may not build (ADR 0251)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 12:08:02 +02:00
jochen 3449775cb6 ADR 0252: a module puts an account in a group, and the mesh says when a new login is needed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Issue 247 left adding the operator's account to a daemon's group a sudo step by hand. Decide that
a module declares it with the user resource's groups, that the mesh gives back only what it added,
and that the node-engine says relogin needed; amend to-be 41 with WP6 and resolve the issue.
2026-10-08 12:04:50 +02:00
jochen f1e2ebce7a Record how the registries keep what is named, so nothing piles up unseen (ADR 0251, to-be 51)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-08 11:51:59 +02:00
mesh-admin f2201beb40 Merge pull request 'Issue 315: a failed unit of a module was never raised' (#199) from issues/315-a-failed-unit-was-never-raised into main 2026-10-08 09:40:28 +00:00
mesh-admin bd14e1b021 Merge pull request 'Issue 313: history adopted at the switch was held for a person; ADR 0250 retires it' (#197) from fix/313-retire-adopted-history into main 2026-10-08 09:36:53 +00:00
jochen 28aabd8621 Issue 315: say the node-engine's repository as an identifier
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 11:30:30 +02:00
jochen f78e21c5f9 Issue 314: a large answer was lost on its way back; issue 200 resolved by 265's fix
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
An answer over the bus's max_payload was refused inside the controller and
every caller timed out, while calls said answered. Diagnosed and proved (R314),
and told apart from issue 200, whose permission refusal issue 265 already fixed.
2026-10-08 11:30:10 +02:00
jochen 7121d0737e Issue 315: a failed unit of a module was never raised
Liveness judged only long-running resources, so a module whose daemon
is a package's unit failed at every login while its machine read
healthy, and a degraded service manager was said by nothing.
2026-10-08 11:30:08 +02:00
jochen 67bb28da4e Issue 313: history adopted at the switch was held for a person; ADR 0250 retires it and ends owed work the forge cannot do
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/313-retire-adopted-history stopped: a member was stopped
2026-10-08 11:20:44 +02:00
mesh-admin b11219e3de Merge pull request 'Issue 309: two order rules ordered one pair both ways; ADR 0249 gives them a precedence' (#194) from issues/309-two-order-rules-ordered-one-pair-both-ways into main 2026-10-08 09:11:45 +00:00
mesh-admin c6e76015b7 Merge pull request 'Issue 310: a pull request ran its own copy of its repository's check' (#196) from issues/310-a-pull-request-ran-its-own-copy-of-its-check into main 2026-10-08 09:10:27 +00:00
jochen a0765c0b13 Issue 310: a pull request ran its own copy of its repository's check
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
The build seat ran the merge-check.sh in the change's head, so a branch older
than the script, or one that gutted it, merged untested; and the media
catalogue did not require mesh/repo-check at all. Located, fixed on branches,
replayed as R310; the protection change awaits the operator.
2026-10-08 11:00:44 +02:00
mesh-admin c0662f1171 Merge pull request 'Issue 312: no tool said a machine's disk load' (#195) from issues/312-no-tool-said-a-machines-disk-load into main 2026-10-08 08:55:40 +00:00
jochen 7839703b60 Issue 312: no tool said a machine's disk load, so issue 306 could not measure its disk
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 10:48:44 +02:00
jochen a3bedeb227 Issue 309: two order rules ordered one pair both ways; ADR 0249 gives them a precedence
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
A group of the controller, the node-engine and the lab, each ready, was
refused as a cycle: built by put the controller's member first, engine
before controller the node-engine's. ADR 0249 ranks the rules (declared,
then built by and version skew, then engine before controller) and
supersedes ADR 0239's sentence that made a declared order against an
inferred one a cycle; design 47 says the precedence. Resolved by
mesh-controller #134 and mesh-catalog #119, replay R309 (mesh-lab #64).
2026-10-08 10:46:34 +02:00
mesh-admin 47aaae98b1 Merge pull request 'Issue 311: the media catalogue's check ran no tests for its TypeScript modules' (#193) from issues/311-the-media-catalogues-modules-ran-no-tests into main 2026-10-08 08:46:10 +00:00
jochen c67ceacccf Issue 311: the media catalogue's check ran no tests for its TypeScript modules
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
Its check declared only Go, so ten modules merged with nothing of their own run, and a
Go module without tests would have passed too.
2026-10-08 10:41:31 +02:00
mesh-admin fcb76ca29d Merge pull request 'Issue 306: a check on the control node ran past its timeout' (#191) from issues/306-a-check-on-the-control-node-ran-past-its-timeout into main 2026-10-08 08:34:59 +00:00
mesh-admin 37e82fb9fc Merge pull request 'Issues 307, 308: the lab's beds kept the bus and runtime the mesh had left; a lab unit test's verdict depended on the machine' (#192) from issues/307-308-the-labs-beds-and-a-host-dependent-test into main 2026-10-08 08:34:52 +00:00
mesh-admin 879b183268 Merge pull request 'Issue 305: a recheck left the old verdict standing' (#190) from issues/305-a-recheck-left-the-old-verdict-standing into main 2026-10-08 08:34:30 +00:00
mesh-admin 57b24738f3 Merge pull request 'Issue 146: the enrolment-time consumer is answered by issue 208; design 08 names the join's code' (#189) from issues/146-the-enrolment-consumer-and-the-tunnel-join into main 2026-10-08 08:24:28 +00:00
jochen 9c030a714a Issues 307, 308: the lab's beds kept the bus and runtime the mesh had left; a unit test's verdict depended on the machine
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Registered so the lab's state on 2026-10-08 is answered where the mesh's faults are, not only in a pull request.
2026-10-08 10:23:34 +02:00
jochen db0fea6067 Issue 306: a check on the control node ran past its timeout
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Store-bound packages ran five to seven times slower there while the rest
ran alike: the throwaway store flushes to a disk the mesh's own data keeps
busy. And a holder recreated mid-check left its store and bus running.
2026-10-08 10:21:49 +02:00
jochen 573a44a33d Issue 305: a recheck must put the merge check back to pending, or the old verdict merges
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-08 10:12:52 +02:00
mesh-admin 0abca926ea Merge pull request 'ADR 0217, to-be 44: no change to a machine takes effect unseen' (#97) from feat/no-change-takes-effect-unseen into main 2026-10-08 00:06:01 +00:00
mesh-admin af398aef7b Merge pull request 'Issues 303, 304; 240 resolved, 242 located' (#94) from issues/245-246 into main 2026-10-07 23:55:47 +00:00
jochen 43bffdc0c3 Issue 146: the enrolment-time consumer is answered by issue 208's assertion; design 08 names the join's code
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
The branch building ADR 0169 carried a fix making a node's declaration
consumer as it enrols. The controller already asserts every node's
consumer before each send, and the self-check's healer asserts a missing
one again, so the fix is dropped rather than merged and the record says
why. The installer now places the bus's accounts at genesis.
2026-10-08 01:53:07 +02:00
jochen e3caa4e1d4 ADR 0217, to-be 44: no change to a machine takes effect unseen
mesh/delivery-group group feat/no-change-takes-effect-unseen delivered: every member is delivered
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
Two incidents in two days had one shape: a change took effect that nobody saw first (issues 241
and 304). Three guards where the change is made — a settings layer read before it is replaced, a
push that says what it will change, a running module's data move acknowledged — each silent when
nothing is at stake. Accepted by the operator on 2026-10-08.

To-be 44 is in progress in mesh-controller (feat/no-change-takes-effect-unseen), migration 0078,
fitted onto what the controller does since: a whole-mesh push that says so and leaves a machine
waiting for a gate, a person's push that says what it recreates (ADR 0242), a named push's cascade.
The decision index is generated (ADR 0248), so no line is added to its README.
2026-10-08 01:45:49 +02:00
jschoubben 67599df951 Issues 303, 304; 240 resolved, 242 located
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
303: a media server's previews were reached through a link its container never mounted — fixed by
mounting them. 304: settings set replaces the whole layer with nothing to read it first and no
history. 240 is fixed by mesh-controller#47; 242 has backups running and records what the rollout
taught. (First opened as 245 and 246; main has since taken both numbers, and 302 was the last.)
2026-10-08 01:45:39 +02:00
mesh-admin 08a874b55f Merge pull request 'Triage issues 279-298: resolve the fixed, with replays R292 and R298' (#188) from issues/triage-279-298 into main 2026-10-07 23:41:52 +00:00
mesh-admin 78fd323df3 Merge pull request 'Triage issues 228-278: close the fixed, re-check the rest' (#187) from issues/triage-228-278 into main 2026-10-07 23:41:48 +00:00
mesh-admin ed24d950db Merge pull request 'Issues 092–208 triaged on 2026-10-08: 13 resolved, 1 wontfix, the rest re-checked' (#186) from issues/triage-092-208 into main 2026-10-07 23:41:45 +00:00
jochen 64e97d0de1 Triage issues 279-298: resolve the fixed with replays R292 and R298
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
292 and 298 resolve with replays now registered in mesh-lab. The other fixed
core issues say why no replay can be laid over the commit before their fix. The
rest are re-checked against main.
2026-10-08 01:31:13 +02:00
jochen 2d18fa3380 Triage issues 228-278: close the fixed, re-check the rest
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Each issue is resolved with its fix and a replay or the reason none exists, or
re-checked against main and the live mesh on 2026-10-08.
2026-10-08 01:31:10 +02:00
jochen 623fccee23 Triage issues 092-208: close the fixed and the obsolete, re-check the rest
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The operator asked on 2026-10-08 for the deprecated issues to be gone. In hq an issue
is closed, never deleted, so each one is resolved with its fix, closed wontfix with a
reason, or re-checked against main and the live mesh.
2026-10-08 01:31:06 +02:00
mesh-admin 990414c9b8 Merge pull request 'Ignore Python's cache folder' (#185) from chore/ignore-python-cache into main 2026-10-07 23:05:54 +00:00
jochen 5d4e427117 Ignore Python's cache: running the checks wrote it, and one file had been committed by mistake
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-08 01:04:03 +02:00
mesh-admin c6ff3b65b6 Merge pull request 'Issues 301, 302: a push of recorded builds is no repair; the lab's Go replays are checked in Go' (#184) from issues/301-302-a-push-of-recorded-builds-and-the-labs-go into main 2026-10-07 22:26:27 +00:00
jochen 7fde8f4bbe Issues 301 and 302: a push of recorded builds is no repair, and the lab's Go is checked in Go
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
301: S15 counted the operator's pushes of recorded builds, the word ADR 0242
asks for, as repairs and wanted a healer for three causes. 302: the lab's
replays register was never compiled before a merge, because its check ran in
the TypeScript toolchain. To-be 45 §7 and §9 say how each is now decided and
checked.
2026-10-08 00:17:14 +02:00
mesh-admin 69ab2fd7f7 Merge pull request 'Issues 296, 299, 300: resolved, each with its replay' (#183) from issues/296-299-300-resolved into main 2026-10-07 21:55:20 +00:00
jochen 787f428e7e Issues 296, 299, 300: resolved, each with its replay proved to fail before its fix
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 23:44:47 +02:00
mesh-admin ded2173308 Merge pull request 'ADR 0248: the decision index is generated when it is read, and never stored (issue 297)' (#182) from decision/generated-decisions-index into main 2026-10-07 21:44:14 +00:00
jochen f4a76ca291 Issue 297: resolved by hq PR #182 and mesh-catalog PR #115
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 23:39:30 +02:00
jochen 55637ecb95 ADR 0248: the decision index is generated when it is read, so two open decisions no longer conflict 2026-10-07 23:38:34 +02:00
mesh-admin eb54474519 Merge pull request 'Issue 300: a new module is not built at its merge' (#181) from issues/300-a-new-module-is-not-built-at-its-merge into main 2026-10-07 21:01:41 +00:00
mesh-admin 2adbfed90f Merge pull request 'Issue 299: an optional verb makes a holder look silent' (#180) from issues/299-an-optional-verb-makes-a-holder-look-silent into main 2026-10-07 21:01:34 +00:00
jochen aaaef91b7b Issue 300: say delivery plan, the glossary's word
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 22:40:17 +02:00
jochen 9b573f4ca0 Issue 300: a new module is not built at its merge
The change plan said a merge adding a module builds it; the merge built
nothing, so the module stayed unregistered until a hand build.
2026-10-07 22:39:56 +02:00
jochen 6006754172 Issue 299: an optional verb makes a holder look silent
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
D3 raised node-uplink silent on every machine once the seat gained verbs its
holders are not yet required to serve, against ADR 0246's step 1.
2026-10-07 22:29:04 +02:00
mesh-admin dda850863f Merge pull request 'ADR 0247: a machine with a VPN client routes names by domain, through a resolver of its own' (#179) from decision/0247-split-dns-on-a-machine-with-a-vpn-client into main 2026-10-07 19:46:08 +00:00
jochen 6373ca815b ADR 0247: say exactly where the VPN's domains go and do not
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 21:35:40 +02:00
jochen 7e9d0dfe7b ADR 0247: say what is to confirm live about resolved and the resolver file, not a guess
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-07 21:35:21 +02:00
jochen 478a198e9a ADR 0247: a machine with a VPN client routes names by domain, through a resolver of its own
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Research 033 measured the laptop's VPN client and the mesh overwriting each
other's resolver file in nine of nine sessions, each time cutting off one
kind of name. The operator chose a resolver module on a node seat of its
own, installed only where something requires split-dns, with the VPN
client's module carrying the adapter. Graduates research 033 into to-be 50,
and amends connectivity §2, to-be 48 §10, the seats and the glossary.
2026-10-07 21:33:48 +02:00
mesh-admin 9b6479f78f Merge pull request 'Issue 297: two open decisions always conflict' (#177) from issues/297-two-open-decisions-always-conflict into main 2026-10-07 19:21:55 +00:00
mesh-admin 0bd12fc777 Merge pull request 'Issue 298 and ADR 0246: a seat's new verb is promised before it is required' (#178) from issues/298-a-new-seat-verb-deadlocks into main 2026-10-07 19:21:52 +00:00
mesh-admin 3083283e17 Merge pull request 'words.py: a retired word's plural is the word' (#176) from fix/words-match-plurals into main 2026-10-07 19:21:34 +00:00
jochen 91daabaa97 Say the uplink verbs shipped optional, not staged: a progressive insight in ADR 0241, corrected in to-be 48
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The controller that merged (mesh-controller #116) dropped the staged seat and
marked both verbs optional, stored in the seat's row and read back optional.
2026-10-07 21:18:11 +02:00
jochen 4b36234c9b Reword the hosts file in to-be 36, found by the plural on the rebase onto main
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/words-match-plurals delivered: every member is delivered
2026-10-07 21:14:43 +02:00
jochen 350f7dcdc2 Hold a retired word's plural to the word, so alerts and rollouts no longer pass
words.py matched a retired word only when nothing followed it, so the
plural of every retired word passed. The pattern now takes s or es on the
last word; 14 uses in 9 documents are reworded, two of them 'the hosts'
for the node-engines.
2026-10-07 21:14:36 +02:00
jochen e6a17b8bf6 Issue 298 and ADR 0246: a seat's new verb is promised before it is required
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Design 33 said a seat's verbs change additively, and section 3 made each
one a condition of holding, so a verb added in the controller and served
from the catalogue could land in neither order. It happened three times on
2026-10-07. Section 7 now says the three steps and how each is checked.
2026-10-07 21:14:20 +02:00
jochen 57f95fc6bb Issue 297: every open decision conflicts with the next one to merge
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The written decision index is the one file every decision PR edits, at the
end of its topic, so on 2026-10-07 each merge forced the others to rebase
and be checked again. The diagnosis weighs four fixes and recommends
generating the index on read.
2026-10-07 21:11:42 +02:00
mesh-admin 921ff3e6c4 Merge pull request 'ADR 0245: a verb says what it replaces, and the agent is guarded from working round the mesh' (#173) from decision/0242-a-verb-says-what-it-replaces into main 2026-10-07 19:10:00 +00:00
mesh-admin 52ecd8bfd5 Merge pull request 'Issue 296: a plan's clock restarted at every save' (#175) from issues/296-a-plans-clock-restarted-at-every-save into main 2026-10-07 19:04:58 +00:00
jochen 6cacf772dc ADR 0245: a verb says what it replaces, and the agent is guarded from working round the mesh
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The agent reached machines over ssh because search never found the verbs by the commands it knew.
Records the three rules (generated instead-of table, search by replaced command, the shell guard
with the operator's recorded override) and how each is checked; to-be 36 names it.
2026-10-07 21:04:24 +02:00
mesh-admin ede4b29cd1 Merge pull request 'ADR 0241 and to-be 48 §10: a machine says how its network is, and an outside writer of a mesh file is a finding' (#168) from decision/0241-a-machine-says-how-its-network-is into main 2026-10-07 19:04:09 +00:00
jochen 6caa471ed0 Issue 296: the refused step logged and saved on every tick
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 20:47:09 +02:00
jochen c462837ff2 ADR 0241's design: name the node-engine's repository as code (ADR 0244)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 20:33:23 +02:00
jochen fe232388a0 ADR 0241 rule 8: the uplink seat answers what the machine resolves through
A finding about a machine's names needs a way to look further that is not
a terminal on the machine; the uplink seat's holders are where that is.
2026-10-07 20:33:23 +02:00
jochen a22a1661e1 ADR 0241 and to-be 48 §10: a machine says how its network is, and an outside writer of a mesh file is a finding
A VPN client rewrote the laptop's resolver file and every mesh name failed
while each module read healthy: nothing judged the machine under them.
2026-10-07 20:33:23 +02:00
mesh-admin f6aa1ced5e Merge pull request 'Issue 295 and ADR 0242 (proposed): a recorded build moves only by a person's push, and a send says what it recreates' (#171) from issues/295-a-recorded-build-was-carried-by-a-plans-send into main 2026-10-07 18:33:14 +00:00
jochen 998980bbda Issue 295: use the glossary's words (ADR 0244)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 20:24:59 +02:00
jochen be8ea16d28 Issue 295 and ADR 0242: a recorded build was carried by a plan's send, and mail went down with a health check
The plan delivering the first health declarations recreated the database and
the document store (record) and nine of mail's containers at once. Record
held only against a person's push to another machine; it now holds against
every send but a person's, and a send says what it recreates.
2026-10-07 20:24:24 +02:00
mesh-admin 4556d37370 Merge pull request 'ADR 0244: the mesh in domains, one word per thing, checked (graduates research 034)' (#174) from decision/0244-the-mesh-in-domains into main 2026-10-07 18:23:58 +00:00
jochen fbc6405f1e Issue 296: a plan's clock restarted at every save
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
plans counted a plan's time from its last save, and every advance saved it, so a queued build read
as seconds old and was never late; located in the controller, fix in mesh-controller PR #119.
2026-10-07 20:08:03 +02:00
jochen 008014591b Do not track the word check's compiled cache
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 20:07:33 +02:00
jochen 051696e107 Retire "the host" in hq prose only: in the tools it is also an SSH server's word
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-07 20:02:03 +02:00
jochen a28da1734b Graduate research 034: the mesh in domains, one word per thing, checked
ADR 0244 sorts the mesh's concepts into ten domains (ADR 0006's contexts
carry over as domains), keeps machine and node as distinct words, and
makes the glossary the authority with every retired word on its
replacement's Not: line. To-be 49 draws the domains; the glossary is
reorganised by them, its two contradictions removed and the missing
words added. words.py now fails on a retired word in running prose and
on a word defined twice, so the rule is enforced rather than believed;
the 63 documents it failed on are reworded here, and research 034 is
kept as the record of the words it studied.
2026-10-07 19:58:39 +02:00
mesh-admin 476558c687 Merge pull request 'ADR 0243: the agent module removes a home item it did not place only on the person's word, and keeps a copy' (#172) from decisions/0243-the-agent-module-removes-a-home-item-on-the-persons-word into main 2026-10-07 17:48:15 +00:00
mesh-admin d96644e827 Merge pull request 'Issue 294: a quoted value given to the controller's command line cannot hold a quote' (#169) from issues/294-a-quoted-value-cannot-hold-a-quote into main 2026-10-07 17:48:03 +00:00
jochen 9ab4ca66ca ADR 0243: the agent module removes a home item it did not place only on the person's word, so the old rule files can leave without a remote shell
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 19:43:55 +02:00
jochen f2abbb042c Research 034: the mesh in domains
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery stopped: the pull request closed unmerged
The operator asked for one language per domain because the same concept
keeps arriving under different words. Inventory the words in use, test a
domain split, list the clashes with evidence, and describe how the
glossary rule would be checked.
2026-10-07 19:37:44 +02:00
jochen 725ad51606 Issue 294: a quoted value given to the controller's command line cannot hold a quote
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 19:23:08 +02:00
mesh-admin 79cadf6c96 Merge pull request 'Research 033: split DNS with a VPN client' (#167) from research/033-split-dns-with-a-vpn-client into main 2026-10-07 16:54:28 +00:00
jochen 0ab47b9417 Research 033: split DNS with a VPN client
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The laptop's VPN client replaces the mesh's resolver file on every connect and
the node-engine writes it back minutes later, so either the mesh's names or the
corporate ones fail for whole sessions. Measure how the client sets DNS and
weigh how names can be routed by domain without breaking ADR 0223's one-answer
rule.
2026-10-07 18:38:49 +02:00
mesh-admin 30d78ec9a2 Merge pull request 'To-be 48 Phases B to E are built: what each chose, and what is still to read on the live mesh' (#166) from feat/a-module-says-how-it-is-healthy-b-to-e into main 2026-10-07 16:28:45 +00:00
jochen e453da3a3b To-be 48 Phases B to E are built: what each chose, and what is still to read on the live mesh
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 16:17:57 +02:00
mesh-admin 8ff22e4d9b Merge pull request 'Issues 292-293: a drill counted as a repair; a warning on a required check blocked the merge' (#165) from issues/292-293-a-drill-and-a-warning into main 2026-10-07 12:05:42 +00:00
jochen 9cf839c977 Issues 292-293: a drill counted as a repair, a warning on a required check blocking a merge
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 13:56:39 +02:00
mesh-admin eef2c5e082 Merge pull request 'Issues 290-291: a delivery adopted while it waited never got its check; a planned maintenance window failed the applies that met it' (#164) from issues/290-291-what-waits-was-never-asked into main 2026-10-07 11:20:21 +00:00
jochen 553d81be60 Issues 290-291: a delivery adopted while it waited never got its check; a planned maintenance window failed the applies that met it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 13:16:55 +02:00
mesh-admin f8189da489 Merge pull request 'Issue 289: a verb answered during a handover could not run its own build' (#163) from issue/289-a-verb-answered-during-a-handover into main 2026-10-07 00:40:22 +00:00
mesh-admin 781f55329d Merge pull request 'To-be 48 Phase A is being built: in-progress, its code named, what the build chose' (#162) from feat/a-module-says-how-it-is-healthy into main 2026-10-07 00:39:41 +00:00
jochen 0ebfdef50d Issue 289: name the pull requests that fix it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 02:29:32 +02:00
jochen dea72dc4fc To-be 48 Phase A is being built: name its code and say what the build chose
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/a-module-says-how-it-is-healthy delivered: every member is delivered
2026-10-07 02:28:18 +02:00
mesh-admin 7d57ace896 Merge pull request 'Issues 285, 286, 287, 288: the merge gate judged nothing; the seat's check disagreed with agents' machines; a seat and a module of one name were unreachable; the facts still name the installation' (#161) from issues/282-284-the-merge-gate-judged-nothing into main 2026-10-07 00:28:01 +00:00
jochen fd0bb826b3 Issue 289: a verb answered during a handover could not run its own build 2026-10-07 02:14:08 +02:00
jochen a9f396a0a0 Renumber the gate's issues 285-287 (282 was taken on main); issue 288: the facts snapshot still names the installation
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-07 02:04:46 +02:00
jochen 41a0e77748 Issues 282-284: the merge gate judged nothing, the seat's check disagreed with agents' machines, a seat and a module of one name were unreachable 2026-10-07 01:59:45 +02:00
mesh-admin 35d81e962e Merge pull request 'Issue 282: a secret on a command line is kept in the runtime's events' (#160) from issue/282-a-secret-on-a-command-line into main 2026-10-06 23:40:55 +00:00
jochen 124943bdc3 Issue 282: a secret on a command line is kept in the runtime's events
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The mosquitto module passed its broker's admin password to every docker
exec as an argument, which the container runtime records in its events.
Records the cause, the other modules found doing the same, the fix
proposed in mesh-catalog #100, and where the leaked value went.
2026-10-07 01:38:37 +02:00
mesh-admin e23efb63a9 Merge pull request 'ADR 0240 and to-be 48: a module says how it is healthy, and the node-engine judges it' (#159) from decision/0240-a-module-says-how-it-is-healthy into main 2026-10-06 23:35:39 +00:00
jochen 0bf579d99c ADR 0240 and to-be 48: a module says how it is healthy, and the node-engine judges it
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
The gate judged a module by what the mesh sees from outside, so a crash loop and an
eleven-hour silent web app passed it. Graduates research 032 on the operator's word.
2026-10-07 01:33:06 +02:00
mesh-admin 2e8b7321c7 Merge pull request 'Research 032: a module says how it is healthy — evidence, options, recommendation' (#158) from research/032-a-module-says-how-it-is-healthy-findings into main 2026-10-06 23:18:40 +00:00
jochen ef6ab814b0 Research 032: measure module health on the live mesh and propose a decision
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Liveness is judged nowhere and readiness cannot be declared; the evidence,
options and a graduation-ready recommendation say how a module states it.
2026-10-07 01:18:00 +02:00
mesh-admin 4405a3048c Merge pull request 'ADR 0239: a waiting walk is said by the controller; Phase B built' (#157) from design/0239-a-waiting-walk-is-said into main 2026-10-06 22:47:15 +00:00
jochen 1fcd238cdf ADR 0239: a waiting walk is said by the controller; Phase B built
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
A walk mesh-delivery never lets go waited for ever with nothing open; the
controller now says it itself (S16), and the delivery's own stalls are its
conditions too (D14, H2 through close).
2026-10-07 00:14:09 +02:00
mesh-admin c51a3da5d5 Merge pull request 'ADR 0239: a delivery is owned by mesh-delivery and runs from commit to delivered' (#156) from design/0239-a-delivery-is-owned-by-mesh-delivery into main 2026-10-06 22:07:18 +00:00
jochen e1b95d09cd ADR 0239 and to-be 47: as built — registered at the walk's start, the forge asked through its tools
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery delivered
A build registered before its turn is carried by the next send to its
machines, so a published delivery asks nothing until its walk starts; the
rows the build needed (appeared, adopted, held on no walk, held then go)
and Phase B's verbs-without-probe are now said.
2026-10-06 23:58:29 +02:00
jochen 16b187d4c3 ADR 0239: a delivery is owned by mesh-delivery and runs from commit to delivered
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
One owner for one commit's journey, so 'did my change go out' is answered by
one module instead of five records joined by hand; delivery groups make the
cross-repository order the mesh enforces instead of the person merging.
2026-10-06 23:18:31 +02:00
mesh-admin 7216ffc825 Merge pull request 'ADR 0238: a commit is the build at hand — one commit, one change plan, checked off the trunk and published only on it' (#155) from design/0238-one-commit-one-change-plan into main 2026-10-06 21:00:50 +00:00
jochen 2221be11a6 ADR 0238: a commit is the build at hand — one commit, one change plan, checked off the trunk and published only on it
mesh/merge-gate the mesh is checking this head against every machine
mesh/delivery delivered
Turning the merge check on for every repository showed it asked the wrong questions: a
repository chose whether it was checked, the gate mapped a change onto modules its own
way, the shared-code rule rebuilt 103 modules for a root script, and nothing kept a
commit off the trunk from becoming a module's version. Records the operator's decisions,
narrows ADR 0237 decision 4, revises to-be 45 §9 and Phase 5 and to-be 30, closes issue
280's left-open, and gives this repository its own merge-check.sh.
2026-10-06 22:54:09 +02:00
mesh-admin 9bd54ee05d Merge pull request 'Issue 281: a tier sent one module at a time blamed a module for its machine' (#154) from issues/281-a-tier-sent-one-module-at-a-time-blamed-a-module-for-its-machine into main 2026-10-06 20:26:05 +00:00
jochen 0555508020 Issue 281: a tier sent one module at a time blamed a module for its machine
mesh/merge-gate the mesh is checking this head against every machine
mesh/delivery delivered
2026-10-06 22:25:22 +02:00
mesh-admin 76c00c15e2 Merge pull request 'Issue 280: a rebuild of an unchanged source was read as a new bus' (#153) from issues/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus into main 2026-10-06 20:12:06 +00:00
jschoubben 897bcce502 Issue 280: a rebuild of an unchanged source was read as a new bus
mesh/merge-gate the mesh is checking this head against every machine
mesh/delivery delivered
An image is not byte-reproducible, so ADR 0236's no-move rule never held
for one; the rule now also reads a build's source. Progressive insight on
ADR 0236 says what the rule assumed and what stands.
2026-10-06 22:11:27 +02:00
mesh-admin 163b4f6c48 Merge pull request 'Issue 279: a folder cannot be owned by the account a module runs as' (#151) from issues/279-a-folder-cannot-be-owned-by-the-account-a-module-runs-as into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 19:28:46 +00:00
mesh-admin ca86423664 Merge pull request 'ADR 0237, to-be 45 Phase 5: a change is judged against the mesh that runs before it merges' (#152) from feat/checks-before-merge into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 19:19:22 +00:00
jochen d245df7dea ADR 0237, to-be 45 Phase 5: a change is judged against the mesh that runs before it merges
The operator approved Phase 5. Decides what the design left open: the build seat runs the
merge check, the facts live in the artifact store, the merge gate composes the mesh as it is
and with the change and judges only what the change adds, a replay lives where its incident
is, and the controller's tests run a bus of their own at the mesh's release. A core issue now
resolves only with a replay or a stated reason, checked by cycle.py.
2026-10-06 21:10:54 +02:00
jochen 5e2b520307 Issue 279: a folder cannot be owned by the account a module runs as 2026-10-06 20:57:38 +02:00
mesh-admin be0754ec7f Merge pull request 'Research 032: a module says how it is healthy' (#150) from research/032-a-module-says-how-it-is-healthy into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 18:52:42 +00:00
jochen 5e54632626 Research 032: a module says how it is healthy
The release gate judges a module from outside (applied, no new condition,
tools answer); a container that restarts in a loop or a service that fails
its own work passes it. Investigate a health declaration in every manifest.
2026-10-06 20:50:25 +02:00
mesh-admin 68e432ec0b Merge pull request 'Issue 278: a module held by no machine was read as shared code; ADR 0236's open question answered' (#148) from issues/278-a-module-held-by-no-machine-was-read-as-shared-code into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 18:28:48 +00:00
mesh-admin 578e4ce8b3 Merge pull request 'To-be 45 §7: a hand act a person decides by design is no repair, by the verb that recorded it' (#149) from design/45-s15-a-persons-decision-is-no-repair into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 18:14:48 +00:00
jochen 2d56eae2f1 To-be 45 §7: a hand act a person decides by design is no repair, by the verb that recorded it
Planned bus upgrades and rotations after one leak raised healer-wanted,
because the exemption was keyed on two causes. Progressive insight: the
exemption is read from the verb table; the decisions stand.
2026-10-06 20:14:09 +02:00
jochen 0333aac134 Issue 278: a module held by no machine was read as shared code
The 103-module rebuild ADR 0236 put down to the build agent came from a file of the catalogue's
reference module, read as shared because its definition was not in the merge. Records the issue,
corrects ADR 0236's open question as a progressive insight (the tiering was right; the build agent
never widens a plan) and amends to-be 30's rule for what a merge changed.
2026-10-06 19:57:21 +02:00
mesh-admin 62d4b1e5d4 Merge pull request 'ADR 0236, to-be 45 Phase 4: a build is judged on its first machine and put back by something other than itself' (#146) from feat/core-upgrades-that-roll-back into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 17:15:09 +00:00
jochen 4ee06bd99f ADR 0236: no build reaches a machine without a gate, and a release plan walks what waits
The coordinator's review: at the switch to roll, every send would carry the old default's
backlog unjudged. Measured: 88 of the 103 rebuilt modules were byte-identical, and no
build waited on any machine.
2026-10-06 19:12:51 +02:00
jochen 301c551888 ADR 0236: no send reaches the bus's machine while a new bus build waits
Found live the same day: a plan's send of the catalogue carried the bus's rebuilt image
to the control node and restarted the bus unasked.
2026-10-06 19:12:51 +02:00
jochen 235330ce00 ADR 0236, to-be 45: a build is judged on its first machine and put back by something other than itself
Phase 4 of to-be 45, started by the operator: the core's health as probes, a gate on
every plan's first machine, the rollback there once per build, the witness's contract
with the host, the bus as a step a person starts, and — with those in place — rolling
out as the default, keeping record where a module says why. 47 pushes by hand in one
day are what it ends.
2026-10-06 19:12:51 +02:00
mesh-admin cfac4ac825 Merge pull request 'Issue 277: one unanswered question was an urgent alert nobody could read' (#147) from issue/277-one-unanswered-question-was-an-urgent-alert-nobody-could-read into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 16:47:41 +00:00
jochen 9092cbda38 Issue 277: one unanswered question was an urgent alert nobody could read
Record the single-sample flaw found across the probes and watchdogs, and
the summaries that carried addresses and paths past the operator channel's
content rule; amend to-be 45 §4 with the two-look rule and the summary rule.
2026-10-06 18:45:19 +02:00
mesh-admin 3f32462434 Merge pull request 'Issue 276: the audit logger and the usage store retry, and lose no event' (#145) from issues/276-the-audit-logger-and-usage-store-retry into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 16:38:07 +00:00
jochen 72fcdc366f Issue 276: the audit logger and the usage store retry, and lose no event
The operator decided the two consumers that took a failed write must retry and never lose an event: record how (a thrown write, a spool that takes the last delivery and replays, idempotent writes, a bound that borrows max-deliveries), and why the module counts its own deliveries.
2026-10-06 18:37:28 +02:00
mesh-admin 84707a783a Merge pull request 'ADR 0235: the bus is backed up by its own snapshot of each stream' (#144) from feat/bus-snapshot into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 16:24:54 +00:00
jochen e65daf6f4d ADR 0235: back the bus up by the server's own snapshot of each stream
Resolves ADR 0233's flagged item (the bus's streams copied as live files) as
a progressive insight pointing here. The bus module's own account is granted
the snapshot API and nothing else; restore builds a new store beside the live
one for a person to swap in. To-be 43 gains the bus's section and its restore
steps; design 25 and to-be 45 point to it.
2026-10-06 18:23:40 +02:00
mesh-admin 40e7911da2 Merge pull request 'Issue 276: a handler that did its work was offered it five times' (#143) from issues/276-a-handler-that-did-its-work-was-offered-it-five-times into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 16:17:06 +00:00
jochen fef40505ae Issue 276: a handler that did its work was offered it five times
A refresh answered with an empty body was read as JSON, so the media server module's handler threw after scanning; each finished download was offered five times and raised a false max-deliveries. Records the one rule for taking an event, and where the fixes are.
2026-10-06 18:15:58 +02:00
mesh-admin 3e32db519f Merge pull request 'Issue 275: a machine waiting for its push was said to be urgent' (#142) from issues/275-a-machine-waiting-for-its-push-was-said-urgent into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 16:07:18 +00:00
jochen 743de7572c Issue 275: a machine waiting for its push was said to be urgent 2026-10-06 18:06:43 +02:00
mesh-admin 754e0fb8e0 Merge pull request 'ADR 0233: a module declares the data it holds, and the mesh protects and watches it' (#141) from feat/a-module-declares-the-data-it-holds into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 15:11:21 +00:00
jochen 082693fb25 ADR 0233: how data is measured, and what the first night costs
Nothing large is walked; the first night's size on the home server is measured and stated, the
previews are left out, the platform database is dumped, and the bus's live copy is flagged.
2026-10-06 17:06:18 +02:00
jochen 7188d443bc ADR 0233: a module declares the data it holds, and the mesh protects and watches it
The operator's direction after issue 273: what data a module keeps, how precious it is and how it
is protected is said once, in the manifest, and backups, sticky bindings, retirement on unassign
and the self-check's watch are derived from it. Amends to-be 18, 32, 43 and 45.
2026-10-06 17:06:18 +02:00
mesh-admin 3f62e2bcc1 Merge pull request 'ADR 0234, to-be 46: the mesh holds a conversation with its operator' (#140) from decision/0234-the-mesh-holds-a-conversation-with-its-operator into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 14:59:31 +00:00
jochen 54fe510b54 ADR 0234, to-be 46: decide factor recovery, addressing, references and who is the operator
The operator found four gaps that would bite on first use: a lost phone
locked the mesh's only person out, an operator message had no addressee,
asks could not name the specifics they need, and the operator was a
holder's flag. Each is now a rule with its check and its build phase.
2026-10-06 16:58:59 +02:00
jochen fb30b75f20 ADR 0234, to-be 46: let the operator answer, and let only the controller act on an answer
Research 028 graduates: the mesh needs to ask as well as tell, over
channels that are seats rather than code inside one notifier, and an
action a person must approve cannot rest on a desk click an agent can
forge. TOTP verified by the controller is the proof; a key stays optional.
Amends to-be 45 section 5 as ADR 0227 left it to.
2026-10-06 16:39:11 +02:00
mesh-admin e77ce351cd Merge pull request 'Research 028: a conversation with the operator over channels that are seats; Telegram as first holder' (#139) from research/028-telegram-channels-and-triggers into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 14:33:22 +00:00
jochen b140ac9af7 Research 028: frame it as a conversation with the operator; add asks, work context and the desk
The operator widened the direction: asks need not be about permission, the work context picks
the channel, and desktop buttons should serve at the desk. Make the conversation the core model
and keep authorisation as a layer on top, with proofs a desk click alone cannot give.
2026-10-06 16:24:23 +02:00
jochen 62db332b3e Research 028: channels and intake as seats, decisions from a channel, Telegram as first holder
The operator asked to approve and reject through Telegram, for a generic shape in which a
channel holds a seat, for input triggers, and for capabilities that decide which actions travel
where. Widen 028 rather than open a new effort, since its Q1, Q7 and Q8 own the question.
2026-10-06 16:17:04 +02:00
mesh-admin 0c55ebf125 Merge pull request 'Issue 274: a provider is granted only the consumers bound to it' (#138) from issues/274-a-provider-was-granted-consumers-bound-elsewhere into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 14:09:29 +00:00
jochen c818e9c766 Issue 274: a provider is granted only the consumers bound to it 2026-10-06 16:08:30 +02:00
mesh-admin f197fcf146 Merge pull request 'Issue 273 and ADR 0232: a binding to a consumer's data moves only by a person' (#137) from issues/273-a-rule-for-the-resolver-moved-a-machines-databases into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 13:22:42 +00:00
jochen 732a17e112 Issue 273 and ADR 0232: a binding to a consumer's data moves only by a person
Issue 258's seat rule, written for the resolver, re-bound a machine's database consumers to the store
holding the seat elsewhere, where each was made an empty database, and nothing said so. Record the
incident, limit 258's rule to provisions that keep nothing, and decide that a binding to data is
kept and moved only by a pin.
2026-10-06 15:21:42 +02:00
mesh-admin cf9eec76c9 Merge pull request 'ADR 0226: the private network is assigned by its own name, and the proxy names its public issuer' (#128) from decision/0226-the-private-network-by-its-own-name into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 13:04:28 +00:00
jochen 4f981cc2d7 ADR 0226: say a retired definition is not ADR 0230's retired consumer
The two retirements are easy to confuse; the second deletes nothing a person must approve.
2026-10-06 15:02:18 +02:00
jochen 905ac0f0e4 ADR 0226: assign the private network by its own name, let the proxy name its public issuer
Two modules existed only to say one thing each: networking required mesh-wireguard and nothing else,
and public-acme pointed the one proxy at Let's Encrypt. dhcpcd and cloudflare-dns are assigned
nowhere. Retire all four, keeping every machine's network and every certificate as they are.
2026-10-06 14:59:26 +02:00
mesh-admin 89c7a4263d Merge pull request 'ADR 0230: a consumer the mesh stops asking for is retired, and deleted only by a person' (#135) from feat/retired-consumers into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 12:46:32 +00:00
jochen 4801aa87c1 Say that a retirement decision never asks for a healer
Approving and deleting are a person's act by design (ADR 0230); S15 of to-be
45 now says it leaves them out.
2026-10-06 14:46:09 +02:00
jochen da136ab40e ADR 0230: ten minutes as well as five passes, and mark-only where a provider cannot disable
Five passes are 25 s, shorter than a real hiccup; and a TypeScript provider's
remove can destroy, so retiring never calls it. Every TypeScript provider is
mark-only until it gains a retire that disables.
2026-10-06 14:40:40 +02:00
jochen 3904ef88bf ADR 0230: a consumer the mesh stops asking for is retired, and deleted only by a person
The operator's decision replaces ADR 0229's hourly release: the mesh no longer
finishes alone what its bound exists to question. To-be 45 and the module
protocol amended; 0229 and 0224 point to where their mechanism moved.
2026-10-06 14:40:40 +02:00
mesh-admin 4cdee43f69 Merge pull request 'ADR 0231: a healer acts on what observation raised, and only observation says it worked' (#136) from feat/a-core-that-cannot-fail-silently-phase-3 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 12:29:16 +00:00
jochen 6653f63779 ADR 0231: a healer acts on what observation raised, and only observation says it worked
Phase 3 of to-be 45 was built: the details its section 7 left open are
decided in the record and the design says them as built — where budgets are
counted, the settle, what a healer may send, H4 on D6's consumer-behind, the
report verb's shape, the mesh-wide brake and S15.
2026-10-06 14:28:08 +02:00
mesh-admin 343fc200f1 Merge pull request 'ADR 0229: the core's order is a lease the store remembers, and an epoch a machine is sent once it reads one' (#134) from decision/0229-the-cores-order-and-the-brake into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 10:43:13 +00:00
jochen c6e7f2c911 ADR 0229: the core's order is a lease the store remembers, and an epoch a machine is sent once it reads one
Phase 2 of to-be 45 met questions its paragraphs do not answer: a strict
node-engine refuses an unknown key, a lease bucket can be raised again from
nothing, a command at a shell sends declarations too, and the bus's grant
is composed by the controller that needs it. The answers, the wire contract
and the withdrawal brake go into to-be 45 with issue 270's Phase 1
decisions; issue 272 records that the SDK's provider loop says nothing on
the bus.
2026-10-06 12:26:33 +02:00
mesh-admin c99eb82e0e Merge pull request 'ADR 0228: a value given by hand lives only until its module's first good start' (#133) from feat/a-given-secret-lives-until-the-first-good-start into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 10:16:29 +00:00
jochen 9c7ec6ba75 ADR 0228: a value given by hand lives only until its module's first good start
The controller read 0113 as refusing to replace any value given to it, so
two leaked secrets only their own modules read could not be rotated without
a person handling a new value. Amends to-be 13 and points 0113 at it.
2026-10-06 12:15:29 +02:00
mesh-admin 2ccfd1d73d Merge pull request 'Issue 271: a new consumer replayed the morning to the operator' (#132) from issues/271-a-new-consumer-replayed-the-morning-to-the-operator into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 10:06:28 +00:00
jochen 04664e6acc Issue 271: a new consumer replayed the morning to the operator
The operator-channel's holder said three hours of cleared conditions as new
the moment its consumer was made; recorded so the class has a home, with
its fix in mesh-catalog #88 and the to-be 45 §5 amendment still owed.
2026-10-06 12:04:23 +02:00
mesh-admin 95c024b7c6 Merge pull request 'Issue 208: the bus's objects asserted on every send' (#131) from issues/208-bus-objects-asserted-on-every-send into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 09:47:02 +00:00
jochen e6e9d4dcc3 Issue 208: the bus's objects asserted on every send; fix pointer, the module half, Phase 3 note 2026-10-06 11:46:27 +02:00
mesh-admin 358cce00a7 Merge pull request 'Issue 270: Phase 1 watches signals that come later; to-be 45 in progress' (#130) from issues/270-phase-1-watchdogs-whose-signals-come-later into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 08:37:47 +00:00
jochen fc9b252949 Issue 270: Phase 1 watches signals that come later; to-be 45 in progress
Building Phase 1 of to-be 45 found four of its rows depend on later phases
and S9 hears only the controller's own connection; the decisions the build
made that the design does not state are recorded for an amendment.
2026-10-06 10:18:33 +02:00
mesh-admin aa52a0b6bb Merge pull request 'To-be 45 in progress; issue 269: the controller cannot write a cancelled set' (#129) from feat/a-core-that-cannot-fail-silently-phase-0 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 07:19:15 +00:00
jochen a6c0f002a5 To-be 45 in progress; issue 269: the controller cannot write a cancelled set
Phase 0 is being built in mesh-controller and mesh-host. Granting the
controller its own buckets showed that its grant never covered the work
queues' cancelled sets, so every cancel times out.
2026-10-06 03:04:36 +02:00
mesh-admin 213dcbafe9 Merge pull request 'Research 031: a core that cannot fail silently' (#125) from research/031-a-core-that-cannot-fail-silently into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 00:32:15 +00:00
jochen 0f163a3e7c Research 031 graduates: ADR 0227 and to-be 45
The operator approved the conclusion: the core must say when it is wrong
instead of waiting to be looked at. Record the nine rules with their
checks, and specify the parts and the six phases a build can take.
2026-10-06 02:31:21 +02:00
mesh-admin 09fa192cac Merge pull request 'ADR 0225: a consumer's identity is bounded by the provision it requires' (#127) from decision/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 00:28:47 +00:00
mesh-admin 3e55277aa9 Merge pull request 'Issue 268: letta printed its passwords into its log' (#126) from issues/268-letta-printed-its-passwords into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-06 00:25:32 +00:00
jochen 8fade781a7 ADR 0225: a consumer's identity is bounded by the provision it requires
Issue 263: one global 20-character bound held keyless provisions to an
object store's key, was found only when a provider composed, and then
refused the provider's whole machine. Take ADR 0049's option C, check
overflows before merge, and leave an overflowing consumer out of its
provider's grants instead of refusing the provider.
2026-10-06 02:18:03 +02:00
jochen f6f5563de2 Issue 268: letta printed its passwords into its log
Record the leak, its cause, the fix and the rotation steps the operator
approves once the fix runs, so the exposed secrets are replaced in order.
2026-10-06 02:15:00 +02:00
jochen 97ff655e34 Research 031: open the effort on a core that cannot fail silently
The operator's mandate: races, ignored commands and silence keep recurring.
Classifies 48 recent issues by class, proposes nine checkable principles and
a phased roadmap, for review before anything graduates.
2026-10-06 02:09:55 +02:00
mesh-admin 202f2aa144 Merge pull request 'Issue 267: a reconcile's report overtook the apply that followed it' (#124) from issues/267-a-reconcile-report-overtook-the-apply into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 23:47:11 +00:00
jochen d77399055c Issue 267: a reconcile's report overtook the apply that followed it
Record why a release plan waited on a report it had already been given,
and point issue 264 at it, since 264's fix was suspected and is not the
cause.
2026-10-06 01:46:33 +02:00
mesh-admin 3ba75a312b Merge pull request 'Issue 266: a merge on the bus was never handed to the controller' (#123) from issues/266-a-merge-the-bus-never-handed-the-controller into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 23:33:19 +00:00
mesh-admin 64c2d3a0a1 Merge pull request 'Issue 265: a push outlived its caller, and the bus refused its answer' (#122) from issues/265-a-verb-outlasting-its-caller into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 23:33:12 +00:00
jochen 14dabf60ee Issue 266: a merge on the bus was never handed to the controller
The bus server's multi-filter consumers skip messages on 2.10; record the
evidence, the reproduction, and the two fixes (server upgrade, catch-up).
2026-10-06 01:30:08 +02:00
jochen 1d8745b54d Issue 265: a push outlived its caller, and the bus refused its answer 2026-10-06 01:14:56 +02:00
mesh-admin f9e0517e0f Merge pull request 'Issue 244: located — the console drops a mesh seat's node, and a push ran on every machine' (#121) from issues/244-located into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 22:55:03 +00:00
mesh-admin 88f7f79fbb Merge pull request 'Issue 179 recurred; ADR 0224: a provider that keeps failing a consumer is a problem the controller reports' (#119) from issues/179-recurred-and-safety-nets into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 22:43:01 +00:00
jochen 3ee27b870d Issue 244: located — the console drops a mesh seat's node, and a push ran on every machine
The same empty schemas recurred for push, plan, assign, pin and settings;
for push the dropped machine became a push of the whole mesh. Record the
evidence, the cause in the console's address form, and the open fixes.
2026-10-06 00:38:37 +02:00
mesh-admin 9bcd714ee6 Merge pull request 'Issue 264: a self-updating engine lost the report of the apply that delivered it' (#120) from issues/264-self-update-lost-report into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 22:31:47 +00:00
jochen 3a8c23ef1a Issue 264: a self-updating engine lost the report of the apply that delivered it
Recorded so the release plan's stall on the anchor has a cause and a fix on
record; the fix is in mesh-host and not yet merged.
2026-10-06 00:30:45 +02:00
mesh-admin fd4df96c21 Merge pull request 'Design: resolv.conf is the uplink's holder's; a machine's names are node-hostname's (ADR 0223 parts 2–3)' (#117) from design/0223-uplink-resolv-and-hostname into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 22:20:32 +00:00
jochen 1e02593adf Issue 179 recurred; ADR 0224: a provider that keeps failing a consumer is a problem the controller reports
The identity provider's admin lost the mesh's password again when its database
moved, and 31,000 silent failures followed. Record the recurrence, the rule
that makes a failing provider visible in status, and the module's self-repair.
2026-10-06 00:14:38 +02:00
mesh-admin aa5a735994 Merge pull request 'Issue 263: every consumer pays for the tightest backend's name limit' (#118) from issues/263-the-identity-limit into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 22:09:58 +00:00
jochen 6db919f789 Issue 263: every consumer pays for the tightest backend's name limit 2026-10-06 00:09:53 +02:00
jochen 9b7fac3084 Design: resolv.conf is the uplink's holder's, a machine's names are node-hostname's (ADR 0223 parts 2 and 3)
The build of both parts is in review; the designs now say how they work and
are checked, and ADR 0223 records that the managers' own DNS mechanisms could
not write the mesh's file.
2026-10-05 23:44:41 +02:00
mesh-admin 947965e750 Merge pull request 'Issues 190, 259, 262: resolved' (#116) from issues/190-259-262-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:56:31 +00:00
jochen 00a0d75ced Issues 190, 259, 262: resolved and verified 2026-10-05 22:56:26 +02:00
mesh-admin 178994ee0d Merge pull request 'ADR 0223: the mesh has two resolvers, and a machine lists only them' (#115) from decision/0223-the-mesh-has-two-resolvers into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:48:09 +00:00
jochen 4f1300fd48 ADR 0223: the mesh has two resolvers, and a machine lists only them
musl asks every listed nameserver at once and takes the first reply, so ADR
0196's public fallback answered NXDOMAIN for mesh names in every Alpine build
on the home server. Decide two mesh resolvers and no public line now; record
resolv.conf moving to the uplink's holder and /etc/hosts with /etc/hostname
moving to one hostname seat as the next steps. Amend to-be 08 and 26.
2026-10-05 22:43:18 +02:00
mesh-admin 6e59281806 Merge pull request 'ADR 0222: a module is told where a mesh seat's holder is reached; the controller writes no file a seat's holder owns (issue 190)' (#113) from issues/190-controller-writes-no-owned-file into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:42:57 +00:00
jochen dae33af8b0 Merge main into issues/190-controller-writes-no-owned-file; index regenerated 2026-10-05 22:42:53 +02:00
mesh-admin 48e4ac9aa7 Merge pull request 'Glossary: node-engine replaces "host agent" and mesh-host' (#114) from glossary/node-engine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:31:18 +00:00
jochen eb7356360f Glossary: the node-engine, the program every node runs, replaces "host agent" and mesh-host 2026-10-05 22:31:13 +02:00
mesh-admin 6719ab029c Merge pull request 'ADR 0221: a push sends no build a policy or a plan holds back, except to the machine it names' (#112) from decision/0221-a-push-sends-no-held-build into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:26:49 +00:00
jochen 93bed2147f ADR 0222: a module is told where a mesh seat's holder is reached; the controller writes no file a seat's holder owns
Issue 190's remaining steps: the runtime's module states the registry trust through
${seat:mesh-artifact-store:reach}, the private network stops writing it, generated resources
meet the collision check, and the rollout is an order rather than one push. ADR 0082 and 0102
get notes saying where their mechanism now lives.
2026-10-05 22:24:15 +02:00
jochen 95ce92c62f ADR 0221: a push sends no build a policy or a plan holds back, except to the machine it names
A named push's cascade sent every machine a build held back by `record` or by
a plan waiting on its first machine, so a change meant to be walked through
the mesh one machine at a time reached all of them at once (issue 259).
Records the decision, narrows ADR 0083's flush with a dated pointer, amends
to-be 30, and locates issue 259 in the controller.
2026-10-05 22:23:29 +02:00
mesh-admin 072489e653 Merge pull request 'Issues 260, 262: a service waits for what it reads; a mesh name has no IPv6 address rather than no name' (#111) from issues/260-262 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:08:22 +00:00
jochen eb4b4256a1 Issues 260, 262: a service waits for what it reads; a mesh name has no IPv6 address rather than no name 2026-10-05 22:08:12 +02:00
mesh-admin 553d0c8893 Merge pull request 'ADR 0220: what a machine asks needs its uplink held, and the retired resolver pieces go' (#110) from decision/0220-resolver-config-needs-the-uplink into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 20:02:59 +00:00
jochen 08643a2128 ADR 0220: resolver config needs the uplink; retired resolver pieces go
The rule that keeps resolv.conf the mesh's was checked by nothing, and a
retired seat and an unused module still read as live options. Amends to-be
26 and 08 to match.
2026-10-05 21:59:28 +02:00
mesh-admin 4fac135a46 Merge pull request 'Issues 257, 261: a reconcile applied an older declaration over a newer one' (#109) from issues/257-261-a-reconcile-applied-an-older-declaration into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 19:44:14 +00:00
jochen f391c5c36c Issues 257, 261: a reconcile applied an older declaration over a newer one 2026-10-05 21:44:09 +02:00
mesh-admin e6fbb86373 Merge pull request 'Issue 258: resolved' (#108) from issues/258-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 19:18:57 +00:00
jochen c8af8d5eb1 Issue 258: resolved 2026-10-05 21:18:49 +02:00
mesh-admin 0e193ebc41 Merge pull request 'Issues 258–260: what the move to one resolver met' (#107) from issues/258-260-the-resolvers-rollout into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 19:15:18 +00:00
jochen a0de734ed6 Issues 258–260: what the move to one resolver met 2026-10-05 21:15:09 +02:00
mesh-admin 55e776a1cd Merge pull request 'Issue 257: a plan waited on a declaration its first machine never reported' (#106) from issues/257-a-plan-waited-on-a-declaration-its-first-machine-never-reported into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 18:44:23 +00:00
jochen c233a1bbac Issue 257: a plan waited on a declaration its first machine never reported 2026-10-05 20:42:41 +02:00
mesh-admin 035d4a27e3 Merge pull request 'ADR 0219: the build queue is controlled through the controller and the build seat' (#105) from decision/0219-the-build-queue-is-controlled into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 18:22:01 +00:00
jochen 77429488b0 ADR 0219: plans say what their builds wait on, and a failed plan can go on 2026-10-05 18:56:18 +02:00
jochen 3944e4f914 ADR 0219: the build queue is controlled through the controller and the build seat
The operator asked for tools to see, cancel, clear, stop, pause, continue,
restart and replay builds; none existed. Queue actions are the controller's,
process actions the build seat's per machine, and every action that drops
work leaves a failed outcome so no plan waits on it. To-be 18 amended.
2026-10-05 18:54:44 +02:00
mesh-admin ab1fbe3202 Merge pull request 'Issues 224, 248-252, 254, 255 resolved; 256 opened and resolved; 253 where it stands' (#104) from records/resolved-2026-10-05 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 16:36:27 +00:00
jochen 0fdcb15200 Issues 224, 248-252, 254, 255 resolved, 256 opened and resolved, 253 where it stands
Each resolved with the pull request that fixed it and what was seen live;
253 waits for real collection, the operator's word to give.
2026-10-05 18:36:19 +02:00
mesh-admin 91aa481269 Merge pull request 'Issue 255: the journal verb read nothing for a system service' (#103) from issue/255-the-journal-verb-read-nothing into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 16:10:02 +00:00
jochen 0627b7f1f3 Issue 255: the journal verb read nothing for a system service 2026-10-05 18:09:44 +02:00
mesh-admin 049b50b66c Merge pull request 'Issue 248: delivery restored; a merge still heard twice has another cause' (#101) from issue/248-a-merge-heard-twice into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 15:55:48 +00:00
mesh-admin 44808ac563 Merge pull request 'ADR 0218 and issues 250-254: delivery in order, and a store that keeps what it says' (#102) from records/delivery-and-store into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 15:55:45 +00:00
jochen f000288147 ADR 0218 and issues 250-254: delivery in order, and a store that keeps what it says
ADR 0218: grants before code, one machine first, a newer merge takes over an
older plan (to-be 30 amended). Issues 250 (a merge announced twice), 251 (the
record's checkout owned by another account), 252 (a merge's changed modules
read wrong), 253 (the collector would delete every kept archive; to-be 18
amended, ADR 0189 corrected as a progressive insight), 254 (plans run over
each other); 249 located.
2026-10-05 17:51:03 +02:00
jochen 80aff0f457 Issue 248: delivery restored; a merge still heard twice has another cause 2026-10-05 17:30:18 +02:00
mesh-admin 400ed8d396 Merge pull request 'Issues 248, 249: the controller's event consumer replayed a week; a new state is refused until a push' (#100) from issues/248-249-delivery into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 15:27:24 +00:00
jochen df2072aed5 Issues 248 and 249: the controller's event consumer replayed a week; a new state is refused until a push
248, located: a consumer made with the server's default replays the whole
stream, and the controller's held every new merge and build behind a week of
old ones. 249, open: a module's new state reaches its bundle before the
grants that let it use it.
2026-10-05 17:16:35 +02:00
mesh-admin ae58570209 Merge pull request 'Issue 247: a module cannot put the operator's account in a group' (#99) from issues/247-a-module-cannot-put-the-operator-in-a-group into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 13:45:52 +00:00
mesh-admin 855c3f9b33 Merge pull request 'Issue 246: the console says a module runs nowhere when a runtime answers late' (#98) from issues/246-the-console-loses-the-largest-runtime into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 13:45:49 +00:00
jochen f93b02900c Issue 247: a module cannot put the operator's account in a group 2026-10-05 15:45:24 +02:00
jochen 9306192f93 Issue 246: the console says a module runs nowhere when a runtime answers late 2026-10-05 15:40:14 +02:00
mesh-admin 8bd56a2820 Merge pull request 'Issue 243: resolved' (#87) from issue/243-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 13:17:44 +00:00
mesh-admin 7497149eed Merge pull request 'Issue 245: behind is what a plan decided and has not yet done' (#96) from issues/245-the-plan-is-the-truth into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 13:17:04 +00:00
jochen 1b9196b807 Issue 245: behind is what a plan decided and has not yet done (the operator's direction) 2026-10-05 15:16:51 +02:00
mesh-admin b3e7191ddc Merge pull request 'Issue 245: status calls a module behind when only its repository moved' (#95) from issues/245-behind-means-its-files-changed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 13:15:57 +00:00
jochen 87f80573b8 Issue 245: status calls a module behind when only its repository moved 2026-10-05 15:15:44 +02:00
mesh-admin 8aeef9969d Merge pull request 'ADR 0189 collects again; issue 244: a verb with an empty schema cannot be called' (#93) from decision/0189-collects-again into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 12:38:40 +00:00
jschoubben 8ade75c550 ADR 0189 collects again; issue 244: a verb with an empty schema cannot be called
The nightly collector is back on the store (mesh-catalog#57). It was out for
a day because while-stopped named the module-local id and the host refuses a
declaration naming a container it does not have, whole; the namespacing was
fixed the same night and the composition was read before anything was sent
this time — plan's distribution.collect shows while-stopped as
distribution.store. novox applied it with no refusal and the registry was
untouched.

The store is 40G at 14:37, the filesystem 56% full; the collector first fires
at 03:30. Deleting a manifest frees no bytes until then, so that is the
number to read against.

244: mesh-controller.plan and .node publish an empty schema and then refuse
with 'needs node', including when node is passed — the console drops what
the schema does not declare. A tool that cannot be called is worse than one
that is absent, because it is listed. Worked around through the login shell,
which is the path the console exists to replace.
2026-10-05 14:38:17 +02:00
jschoubben 30bd65ad7b Merge pull request 'to-be 43: in progress' (#92) from feat/node-backup into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 10:24:16 +00:00
mesh-admin b59d479992 Merge pull request 'ADR 0216: the agent's configuration is registered through its module, at three scopes, and served as one plugin' (#89) from decision/the-agent-configured-through-its-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 09:50:44 +00:00
jschoubben 4f599f361b to-be 43: in progress — the seat, the restic holder and the stores' contributions 2026-10-05 11:47:59 +02:00
jochen f291d113c8 ADR 0216: the agent's configuration is registered through its module, at three scopes, and served as one plugin
Graduates research 029 and amends design 36 (section 8): skills, subagents,
commands, hooks and output styles in one nox-mesh plugin; servers, settings
and instructions in the managed files; mesh, node and home scopes.
2026-10-05 11:45:58 +02:00
jschoubben daf2f6d2b1 Merge pull request 'to-be 43: backups against mistakes (graduates research 030)' (#90) from design/43-backups-against-mistakes into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 09:35:22 +00:00
mesh-admin 4924b26b3c Merge pull request 'ADR 0215: the machine's message bus is a node seat, and it is never restarted live' (#91) from decision/0215-the-message-bus-is-a-node-seat into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 09:33:36 +00:00
jochen d088f8ec2f ADR 0215: the machine's message bus is a node seat, and it is never restarted live 2026-10-05 11:33:20 +02:00
jschoubben 431375d16c to-be 43: backups against mistakes, declared by modules, kept on the machine
Graduates research 030 into a design for ADR 0214, which merged without one and left the cycle
check failing on main.
2026-10-05 11:31:19 +02:00
jschoubben e51d6f1d86 Merge pull request 'Research 030 + proposed ADR 0214: backups against our own mistakes' (#88) from research/030-backups-against-our-own-mistakes into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 09:30:35 +00:00
jschoubben a906cd8e67 ADR 0214: accepted by the operator 2026-10-05 11:30:33 +02:00
jschoubben 3d0ce3e6dd Research 030 and proposed ADR 0214: backups guard against mistakes and stay on the machine
The operator scoped backups to mistakes, not disasters (issue 242). Issue 238: fix the failed logins
at their source instead of exempting the operator's address.
2026-10-05 10:07:36 +02:00
jochen 9197b99153 Issue 243: resolved by nodes applying any differing binding and the manager never repeating a generation 2026-10-05 10:03:49 +02:00
mesh-admin bc51d8e7aa Merge pull request 'Issue 195: resolved' (#86) from issue/195-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 08:01:20 +00:00
mesh-admin 85c6db0600 Merge pull request 'Research 029: the agent configured through its module, as a plugin the mesh serves' (#85) from research/029-the-agent-configured-through-its-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 08:01:16 +00:00
mesh-admin 9d18968323 Merge pull request 'Issue 243: a rebuilt licence store silences every node' (#84) from issue/243-a-rebuilt-licence-store-silences-every-node into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 08:01:13 +00:00
jochen 127675e2da Issue 243: a rebuilt licence store silences every node
The manager's generation counter restarted with its store after issue 241,
and nodes that wait for a higher number discarded every binding unseen.
2026-10-05 09:59:17 +02:00
jochen 0e87aad949 Research 029: a running session takes the plugin on reload 2026-10-05 09:53:13 +02:00
jschoubben 2a501af0eb Merge pull request 'Issues 239–242: a name taken over, a dry run recorded, every database dropped, no backups' (#83) from issues/239-242 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 00:25:50 +00:00
jschoubben bd7fc40099 Issue 239: name no installation 2026-10-05 02:24:27 +02:00
jschoubben 93d4d29b72 Issues 241, 242: one unreadable grants file dropped every database, and the mesh has no backups
241: the SDK harness read a failed read as "no consumer" and withdrew all seven databases on the
control node; withdrawal destroyed data in seven providers. Fixed in mesh-sdk 0.1.10, mesh-catalog#44
and mesh-host#22; the recovery and what it lost are recorded. 242: nothing backs anything up.
2026-10-05 02:24:11 +02:00
jochen 6ff2440e5d Research 029: the plugin route confirmed on one workstation
Six of seven checks confirmed and autoMode from managed settings very likely;
the agent cannot undo its own managed settings, which a design must allow for.
2026-10-04 23:46:32 +02:00
jochen 65c3315a14 Research 029: the agent's configuration is managed at three scopes
The operator's direction: mesh, node and home. A plugin carries the first
two; the home scope places items the module owns path by path; instructions
follow the same layering.
2026-10-04 17:52:48 +02:00
jochen 854341d4f0 Research 029: the agent configured through its module, as a plugin the mesh serves
Skills, subagents and hooks have no machine-wide place but a plugin, and
hand-copied homes have drifted and gone stale on every machine. Records the
evidence, what the vendor allows, and the options a decision must settle.
2026-10-04 17:46:24 +02:00
jochen d444458bf6 Issue 195: resolved by the controller composing users only for modules that can read an account 2026-10-04 17:37:20 +02:00
mesh-admin dd8b15a0df Merge pull request 'Issue 195: located in the controller's user composition' (#370) from issue/195-diagnosis into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 15:34:11 +00:00
mesh-admin 02b4ca9bec Merge pull request 'ADR 0213: the operator sets the agent's managed settings through the agent module' (#369) from feat/claude-code-agent-settings into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 15:34:09 +00:00
jochen 7cd5b37afe Issue 195: located in the controller's user composition
Records why a module that cannot read an account is no bus user, what else
read those users (durable consumers), and answers the report's questions.
2026-10-04 17:33:27 +02:00
jschoubben aac0da9c0c Merge pull request 'ADR 0199: a module that answers names declares its zone, and a node's hosts file is one module's' (#337) from decision/0199-zones-and-the-hosts-file into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 15:33:07 +00:00
jochen 57b818b669 ADR 0213: the operator sets the agent's managed settings through the agent module
Rules for what the agent may do were set by hand per machine, invisible to
the mesh, and a session cannot loosen its own permissions; design 36 now
takes them as a module setting under the mesh's own keys.
2026-10-04 17:32:18 +02:00
jschoubben 33c10aa85a Issues 239, 240, and 238's diagnosis: a module name taken over, a dry run recorded, the operator banned
239: two repositories defined photos; a rebuild to the catalogue's main replaced the app with a stub
and nothing refused it. 240: a dry-run build was recorded and its definition reached the machine.
238: the forge refused three ssh logins as the operator's account in 31 seconds; nothing in the mesh's
ssh configuration names the forge, and no jail ignores the mesh's own public addresses.
2026-10-04 17:31:29 +02:00
mesh-admin e5042e1e7a Merge pull request 'Issues 237, 238: a self-contradicting assign answer, and the operator's address banned' (#367) from issues/237-238-a-stale-answer-and-a-banned-operator into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 15:06:51 +00:00
jochen 2580fb6839 Issues 237, 238: a self-contradicting assign answer, and the operator's address banned 2026-10-04 17:06:38 +02:00
mesh-admin 4a8a378ef9 Merge pull request 'ADR 0212: a seat says what it receives, and the machine's hotkeys are a seat' (#366) from decision/0212-contributions-to-a-seat-and-hotkeys into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 14:42:31 +00:00
jochen 3f48e685cc ADR 0212: a seat says what it receives, and the machine's hotkeys are a seat 2026-10-04 16:42:21 +02:00
mesh-admin 1e0b9ee11f Merge pull request 'As-is: a module's state, and the agent with its licences; designs 36, 39, 40 implemented' (#365) from design/state-and-licences-as-is into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 14:33:50 +00:00
jochen 1faa63b2d5 As-is: a module's state and the agent with its licences; designs 36, 39 and 40 implemented
What runs since 2026-10-04 and what its first live use showed: refused
requests as timeouts, a late machine reading the whole set, the partial
secrets guard; the agent module and the licence manager on every machine.
2026-10-04 16:32:29 +02:00
mesh-admin 3a359bf99e Merge pull request 'ADR 0211: a machine's power is a node seat, its moments take contributions, and its states are events' (#364) from decision/0211-power-is-a-node-seat into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:58:25 +00:00
jochen ec6d106af8 ADR 0211: a machine's power is a node seat, its moments take contributions, and its states are events 2026-10-04 15:58:16 +02:00
jochen e2b4f3a5c4 Regenerate the decision index 2026-10-04 15:58:09 +02:00
mesh-admin dfb2817fe7 Merge pull request 'ADR 0210: a tool's configuration is its seat holder's, and every other module extends it through the seat' (#361) from decision/0210-a-contribution-depends-on-the-seat-that-receives-it into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:42:56 +00:00
mesh-admin 12742e3a3f Merge pull request 'Designs 36 and 39: the manager's verb is public-key' (#363) from fix/the-verb-is-public-key into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:41:32 +00:00
jochen 5b00bdc7af Designs 36 and 39: the manager's verb is public-key (a seat's verb takes no underscore) 2026-10-04 15:41:23 +02:00
mesh-admin d5a96e5c63 Merge pull request 'Research 028: the mesh's output channel' (#362) from research/028-the-meshs-output-channel into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:36:17 +00:00
jochen 73e6400802 Research 028: the mesh's output channel — how the mesh tells its operator what it noticed 2026-10-04 15:36:03 +02:00
jochen f144ad7be4 To-be 42: who writes what in phase 2 (ADR 0210) 2026-10-04 15:20:01 +02:00
jochen 8394a7bfb1 ADR 0210: a tool's configuration is its seat holder's, and every other module extends it through the seat 2026-10-04 15:19:44 +02:00
mesh-admin 6ac936f170 Merge pull request 'Issues 235, 236: an assignment and a check that let through what then fails' (#360) from issues/235-236-an-assignment-and-a-check-that-let-through-what-fails into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:19:41 +00:00
jochen 3af4179755 Issues 235, 236: an assignment and a check that let through what then fails 2026-10-04 15:17:46 +02:00
mesh-admin dd04729ec5 Merge pull request 'ADR 0209: a login on a node moves that node to its account; an API key is added from any node, sealed' (#359) from decision/0209-a-login-moves-its-node into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:12:01 +00:00
jochen 2e5f40c9fa ADR 0209: a login on a node moves that node to its account; an API key is added from any node, sealed
Traced live: a login to a second account was adopted and left its node
bound to the first, holding a spent refresh token. Designs 36 and 39
amended.
2026-10-04 15:09:56 +02:00
mesh-admin 2f41b4124d Merge pull request 'Issues 233, 234: a stale declaration removed four modules, and the host could not recover' (#358) from issues/233-234-a-stale-declaration-and-a-host-that-cannot-recover into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 13:09:47 +00:00
jochen 9cc00d57ea Issues 233, 234: a stale declaration removed four modules, and the host could not recover 2026-10-04 14:54:50 +02:00
mesh-admin 438162b5a5 Merge pull request 'Issues 225, 226 and 227 resolved with their live proofs; 232 opened and resolved' (#357) from issues/228-photos-authenticates-against-admin into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:37:53 +00:00
jschoubben ab1bd5598e Issues 225, 226, 227 resolved with their live proofs; 232 opened and resolved
225: a grant secret is composed with the account that reads it — the node's
account for a bundle, the declared secrets-owner for a container. Zero EACCES
since 12:30:10 where there had been 4330, both users created, mongodb logging
Authentication succeeded for each. The harness also stops calling a permanent
refusal a race, which is the half that cost three hours.

226: normalising moved to the records, where the provenance is known, and a
reference the sweep will not address is skipped rather than ending the sweep.
The first build after the roll-out collected 200 and said 1126 remain — the
backlog falls with every build instead of standing at 1681 for ever.

227: the three photo modules publish the endpoint they declare, and a
catalogue-wide test makes it a rule: a container publishes only a port its
module declares, or the mesh has nothing to assign and the number escapes.

232 came out from under 225: photos asked for a database its user does not
live in, invisible while no user existed at all.
2026-10-04 12:37:31 +02:00
jschoubben 3f3fb99219 Issue 232: a consumer authenticates against a database its user does not live in
Found fixing 225: with the secrets readable the provisioner created both
users at once, and photos still could not connect because it asks for admin
while its user lives in its own database. The password was never wrong — the
grant secret and the consumer's environment hash identically.

One fault wore the other's clothes: while no user existed anywhere, the
error was a complete account of 225. Worth keeping as a habit — fix the
first and look again.
2026-10-04 12:33:57 +02:00
mesh-admin 3afe619531 Merge pull request 'ADR 0208: the graphical session is one module per piece, on the mesh's seats' (#356) from decision/0208-the-graphical-session-is-modules-on-seats into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:29:20 +00:00
jochen 502cf4839b ADR 0208: the graphical session is one module per piece, on the mesh's seats
Eleven node seats; a display as a provision with the machine's reach; other
modules' lines through the tool's own drop-in directory or ADR 0204's slots,
now also for xinitrc and xresources; the display server's module writes the
session's start.
2026-10-04 12:29:13 +02:00
mesh-admin 0ba68c154e Merge pull request 'Issue 162 resolved: an archive can be undeclared (mesh-host#90)' (#355) from issues/162-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:25:44 +00:00
jochen 460793af1c Issue 162 resolved: an archive can be undeclared (mesh-host#90) 2026-10-04 12:23:06 +02:00
mesh-admin 25de331e9b Merge pull request 'ADR 0207: a module depends on the node seats that apply its resources' (#354) from decision/0207-a-module-depends-on-the-seats-that-apply-its-resources into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:21:46 +00:00
jochen ed5ddcdef6 ADR 0207 extends ADR 0177 (accepted); 0166 stays a reference 2026-10-04 12:21:40 +02:00
jochen e4f80cc3ce ADR 0207: a module depends on the node seats that apply its resources
A service needs node-service-manager held on its node, a package
node-package-manager, a container node-container-runtime: derived from the
resources, never stated; refused at assign, reported at composition until the
three holders are on every node. Glossary: depends on a seat; nothing claims a
package.
2026-10-04 12:21:24 +02:00
mesh-admin d2689c0f86 Merge pull request 'Issues 229 and 230: a rollout cannot be followed through the mesh's tools; a host hand-over loses its report and a plan waits for ever' (#352) from issues/229-a-rollout-cannot-be-followed-through-the-meshs-tools into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:09:34 +00:00
mesh-admin 719aa6bd62 Merge pull request 'Research 026 and 027, issue 231, to-be 42: the graphical session, the system layer, and the order they are built in' (#353) from research/026-027-the-graphical-session-and-the-system-layer into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:08:33 +00:00
jochen ca13f59c88 To-be 42: the machines' modules, in order — every machine's, then the workstations', then one model's 2026-10-04 12:08:26 +02:00
jochen f8a0402485 Merge remote-tracking branch 'origin/main' into research/026-027-the-graphical-session-and-the-system-layer 2026-10-04 12:08:17 +02:00
jochen 9016d88d54 Research 026/027: improve while adopting, the fonts chosen, and a catalogue of the tools each module serves 2026-10-04 12:06:08 +02:00
jochen 6e5dfd2ab8 Research 027/03: the laptop's power management; 026/04: fonts 2026-10-04 12:03:29 +02:00
mesh-admin 872f20d51f Merge pull request 'To-be 40: building the operator's agent and its licence manager as work packages' (#297) from feat/claude-code-work-packages into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 10:01:15 +00:00
jochen 550453c5db Research 026/04: the clipboard 2026-10-04 12:00:20 +02:00
jochen b7aebedc2d Research 026/04: the screensaver, monitor layouts and menus 2026-10-04 12:00:01 +02:00
jochen 27b2d30441 Research 027/03: ~/.ssh as one module's, scripts on every machine, the keyring, mail as events 2026-10-04 11:59:23 +02:00
jochen 82fa5f79ea ADR 0206: a node reports the grant it holds; the manager adopts a licence by refreshing it
The operator's flow: clients publish what their credentials file holds, the
manager takes in a licence it does not own and rotates it from then on. The
token itself cannot be published (design 32 §10, ADR 0201), so a node reports
fingerprints and identity as state and hands the grant over only when the
manager asks; adopting is refreshing, newest login first; bindings with a
generation replace the rotated/switched events. Designs 36 and 39 and to-be 40
amended; a pointer note on ADR 0183.
2026-10-04 11:58:46 +02:00
jochen f6668d76d6 There is no home-scoped module: ADR 0181 and 0182 say so as progressive insights; design 36 and to-be 40: the module declares the two directories it owns
ADR 0173 §2: a module is what it declares, and there are no kinds of module. The two records called
a resource under a home and a module placing one home-scoped; the wording is corrected in place,
marked and dated, the decisions unchanged. Design 36 and to-be 40 now say the module declares
/etc/claude-code and ~/.claude as directories, so the ownership check sees both, and declares no file
under either (mesh-catalog #244).
2026-10-04 11:56:32 +02:00
jochen f5d54db7aa Plan and designs after ADR 0193, 0195 and 0198: bundles are launched and the runtime is their bus; the manager's daemon is a long-running bundle; the console's five tools
The dated note on ADR 0183 now rests on ADR 0193 and 0198 rather than on a bundle having no way to
call: the manager starts every exchange by the operator's direction, through mesh/ask. To-be 40's
WP4 no longer waits on a record — ADR 0198 is it — and the live proofs count the console's five
tools (ADR 0195).
2026-10-04 11:56:32 +02:00
jochen bcf010886d Design 36 §4: the console is registered in the exclusive managed tool-server file, because the managed-settings key refuses a non-https URL 2026-10-04 11:56:32 +02:00
jochen 2eba399e1e To-be 40 revised for the tools refactor; the manager starts every exchange (ADR 0183 dated note, designs 36 and 39)
Design 38's WP1-WP4b ran: the node's tool runtime is live on all four machines as the operator
account, tools are bundles given only their declared words, and a bundle has no bus credential.
So the wait on design 38 WP3 is over, the agent module calls nothing and the manager starts every
exchange (key, hand-over, waiting login, reconcile), and the manager's daemon now waits on WP4c's
record instead. Accounts are stated on all four, sudo -n works for each, the agent is installed on
all four; the plan's WP0 shrinks and WP2 gets a configuration-only live proof before any licence.
2026-10-04 11:56:32 +02:00
jochen d227ed12d2 To-be 40: building the operator's agent and its licence manager as work packages
Designs 36 and 39 say what is built; this says in which order and what proves each step, in the
shape to-be 38 gave the operator's machine. Seven packages: the operator states the facts (accounts,
roles, licences); the console provides its endpoint; the licence manager and the agent module are
built and unit-tested in parallel; the manager goes live on the control node; the agent on one
workstation, with the switch and the predecessor's files removed as the proof of the whole; then the
rest of the nodes and the retirement of the two catalogue modules built on the old placement. The
live proofs wait for to-be 38's WP3, because both modules' tools run in the node's tool runtime
(ADR 0175) and a per-module tool container would rebuild what that record retires.
2026-10-04 11:56:32 +02:00
jochen 8712d666bf Research 026/03: what the predecessor taught; issue 231: a misspelled placeholder is written out as text 2026-10-04 11:49:19 +02:00
jochen 61e70b9395 Research 027: the operator's choices, the hosts file, mounts, and two DHCP clients on one interface 2026-10-04 11:21:35 +02:00
jochen de032e704c Research 026 (the graphical session) and 027 (the system layer)
Evidence from both workstations and all four machines, read-only, and the
questions each must answer: seats and gating for the display stack, who starts
the session with which environment, contributions beyond shells, sway as a
sibling session; the container runtime with docker-compose on workstations
only, software outside the official repositories, secrets in the account's
environment, and three security findings.
2026-10-04 11:17:34 +02:00
jochen 0c2eae07c5 Issue 229: the stale tool list is a connection opened before discovery; the fix is list-changed 2026-10-04 11:01:50 +02:00
jochen f23a71e0d7 Issue 230: a report is also lost when the apply restarts the bus 2026-10-04 11:00:27 +02:00
jochen 9c13c89fa3 Issue 229: new tools never reach an agent's connection, and the console's generic tools are not on it 2026-10-04 11:00:13 +02:00
jochen 2db0ea268d Issue 230: a host that hands over loses its report, and a plan waits for it for ever without saying so 2026-10-04 10:54:35 +02:00
jochen 8d83d94659 Issue 229: a rollout cannot be followed through the mesh's tools 2026-10-04 10:51:45 +02:00
mesh-admin a8ffc2b94b Merge pull request 'Research 025 graduated: ADRs 0203–0205, issue 228, to-be 41 (the shell and the account's environment)' (#351) from feat/the-shell-and-its-environment into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 08:49:36 +00:00
jochen 1dcbdae1c4 Issue 225 → 228: the number was taken on main while this branch was open 2026-10-04 10:30:46 +02:00
jochen c3ec48f85c To-be 41 WP1: directories made inside a home belong to its account 2026-10-04 10:30:23 +02:00
jochen 72eda923c7 ADR 0205: the vendored theme's measured size 2026-10-04 10:30:23 +02:00
jochen c4fedcdbe3 To-be 41 WP1: a shell that refuses logins need not be listed; giving back is never fatal 2026-10-04 10:30:23 +02:00
jochen 0bf70ee8b4 Graduate research 025: the environment and the shell's contributions
ADR 0203: the account's environment is one module's (seat node-environment);
every module contributes variables and PATH entries, rendered by the
controller as a POSIX file and as environment.d.
ADR 0204: shell code is contributed to the login shell in named slots, and
login-shell becomes the mesh's node-login-shell.
ADR 0205: software the distribution does not package ships as a pinned
archive of the module.
Issue 225: undeclaring a user stops a node applying; the shell is never
given back or checked.
To-be 41 carries the work packages; to-be 38 WP5 points to it.
2026-10-04 10:30:23 +02:00
jochen 947b85af5e Research 025: an environment module holds the account's environment
The operator's proposal: one module, holding a mesh seat node-environment, is
the only writer of the account's environment. It renders contributed variables
and PATH entries as a POSIX file shells source and as environment.d for the
graphical session. The shell's contribution shrinks to shell code; the shell
render-then-service-manager-again positions become options weighed against it.
2026-10-04 10:30:18 +02:00
jochen ac6c306df3 Research 025: how a module plugs into the operator's shell
Opened after the zsh module's rollout (to-be 38 WP5) was stopped: every machine
carries the same predecessor-written startup file, the module's block would
duplicate it and drop lines, nothing installs the prompt, and execute never
reads .zshrc. Weighs how modules contribute environment and shell code, where
the operator's own lines go, ordering, and what a contribution is addressed to.
2026-10-04 10:30:18 +02:00
mesh-admin 96df3ccc88 Merge pull request 'ADR 0201 → 0202, and three issues from the night group 8 shipped' (#350) from issues/the-night-of-group-8 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 02:34:32 +00:00
jschoubben bc64c5c187 ADR 0201 → 0202, and three issues from the night it shipped
The derived-value record is renumbered a second time: the key-value-buckets
record took 0201 while this waited to merge, as the bundles record took 0188
before it. Both times free when chosen, taken by the time it landed. cycle.py
caught it; three repositories cite this record, so the number matters.

225 — a provisioner has not been able to read its grant secrets since 01:30,
when a module's own code left its container and the files stayed root's. Four
thousand refusals, each worded as patience, and two consumers unserved. Not
from ADR 0202 or 0189, which landed hours later; dates in the report.

226 — the store's sweep stops at the first reference recorded with an address
and collects nothing. A guard that cannot tell 'I will not ask about this'
from 'it would not answer' stops the wrong amount of work.

227 — the photo app's admin client asks for the port the proxy holds. A module
pinned months behind carries everything its branch gained, the first time
anything makes it move.
2026-10-04 04:33:45 +02:00
mesh-admin be4b5777b8 Merge pull request 'Research 024 and ADR 0201: a module keeps its current state in key-value buckets' (#348) from feat/module-state-on-the-bus into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 01:43:34 +00:00
mesh-admin d882b3568c Merge pull request 'Group 8: ADR 0201 (a provider declares what it derives, issue 124) and ADR 0189 (the store keeps what the records name, issue 108); issue 202' (#305) from feat/the-store-keeps-what-the-records-name into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 01:30:47 +00:00
jschoubben a3523617d3 Review before merge: the multi-holder boundary, the window's open race, the sweep's bounds
ADR 0201 gains the boundary found reading it back: a consumer keeping several
holders of a deriving provider is refused, because the two ends have no way
to agree. ADR 0189 gains two consequences — the sweep is bounded because it
runs inside a build, and an apply arriving mid-window reopens it.

That last one is issue 224, recorded rather than fixed: the host's rule for a
stopped container is to replace it, and while-stopped is the first thing that
makes a stopped container intentional. Both candidate fixes are decisions with
their own cost. Nothing is worse than it was; the store has never collected.
2026-10-04 03:27:32 +02:00
jochen 6b4da63261 Research 024: the composed grants, checked against a server once built 2026-10-04 02:50:02 +02:00
jschoubben 0231974226 Rebased onto main: ADR 0188 renumbered to 0201, and issue 202's evidence re-taken
The bundles refactor took 0188 on main while this waited in a pull request,
and the mesh's own code cites that one, so this record moves. Only the number
moved; the decision is the one taken on 2026-10-02, and the record says so.

Issue 202 re-checked against the refactored main: the fault stands, and the
test that surfaced it now fails one step earlier on issue 203's new credential
guard. Proven again past both — mint the credential, compose twice, and all
eight of dnsmasq's resources appear only with the setting set. ADR 0164 is
noted as the decision that answers half of it, and is not built.
2026-10-04 02:44:58 +02:00
jschoubben 92c029d10e ADR 0189: the store keeps what the records name, and a maintenance step holds its writers still
Issue 108: the artifact store has never collected anything. Fifty-three
repositories on the machine that serves everything else, and the only outcome
of leaving it is a full disk reported as somebody else's failure.

The mesh decides what may go — from its own build records, so it never names
a digest it did not put there — and the store reclaims the bytes in a nightly
window with its server held still. Deletion on the one door takes nothing a
push did not already have.

Designs 18 and 20 amended; issue 108 resolved.

Also issue 202, found running the controller's suite: a module whose required
setting nobody set is left out of the machine in silence, and dnsmasq became
that module this morning.
2026-10-04 02:40:25 +02:00
jschoubben 2a60da821d ADR 0188: a provider declares what it derives for each consumer, and the mesh tells both ends
Issue 124: a value the mesh's own rule produced reached neither end as a
statement. The object store's provisioner derived each consumer's bucket in
its own code; all three consumers transcribed the rule into their own
definitions, one of them wrong, and each of the three also named the machine
it happens to run on.

A served value may now name the consumer the mesh is serving. Design 27
amended; issue 124 resolved.
2026-10-04 02:40:06 +02:00
jochen e1b0bbde91 Research 024 and ADR 0201: a module keeps its current state in key-value buckets
Events miss a machine that joins after them and replay history where only the
latest matters. A module now declares state it owns and reads; the controller
creates the buckets, the runtime serves them on the bundle's channel. Designs 32
and 25 amended; grants measured against a running server.
2026-10-04 02:36:59 +02:00
mesh-admin 8ca09c70d6 Merge pull request 'Issues 213 and 223 resolved' (#347) from issues/213-223-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 00:19:22 +00:00
jochen db71b83711 Issues 213 and 223 resolved: the controller runs as a process, and genesis hands over to it 2026-10-04 02:19:09 +02:00
mesh-admin 9143d0b7c1 Merge pull request 'ADR 0200: genesis pivots to the controller as a container, and the first push hands it to a process' (#346) from decision/0200-genesis-pivots-to-a-container-and-hands-over into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 23:41:36 +00:00
jochen 24a51a8e53 ADR 0200: genesis pivots to the controller as a container, and the first push hands it to a process 2026-10-04 01:41:30 +02:00
mesh-admin f0d7f91d90 Merge pull request 'Design 38: WP4c complete' (#345) from design/38-wp4c-complete into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 23:35:31 +00:00
jochen 8578a06ca8 Design 38: WP4c complete, no module's own code runs in a container 2026-10-04 01:35:16 +02:00
mesh-admin 86083f9c9d Merge pull request 'Issues 215, 221, 222 resolved' (#344) from issues/215-221-222-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 23:29:38 +00:00
jochen 63d328147e Issues 215, 221 resolved with live proof; 222 diagnosed and resolved 2026-10-04 01:29:32 +02:00
mesh-admin fead0ea440 Merge pull request 'Issues 219-223 and design 38 WP4c built' (#343) from issues/219-223-and-wp4c into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 23:14:49 +00:00
jochen df503d1cff Issues 219, 220 resolved, 221 located, 222 and 223 opened; WP4c built and proven 2026-10-04 01:14:44 +02:00
mesh-admin c2fc829822 Merge pull request 'Issues 211, 212, 214, 216, 217 resolved; 215's fix recorded' (#342) from issues/211-217-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 22:25:07 +00:00
jochen 8d8e5c9a7e Issues 211, 212, 214, 216, 217 resolved with their proofs; 215's fix recorded 2026-10-04 00:24:53 +02:00
jschoubben 3ed55a3420 connectivity: name the code that builds the one resolver, zones and the hosts file 2026-10-04 00:23:43 +02:00
mesh-admin affba60b79 Merge pull request 'Issues 219-221: the builder's ordering gaps' (#341) from issues/219-221-the-builder into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 22:16:13 +00:00
jochen 9ddbc4c68e Issues 219-221: the builder's ordering gaps seen while rolling out issue 218 2026-10-04 00:16:08 +02:00
mesh-admin a972db91f0 Merge pull request 'Issue 218 resolved: the runtime follows its membership, and a refused subscription is not fatal' (#340) from issues/218-rollout into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 22:14:10 +00:00
jochen 029698fdc8 Issue 218 resolved: the runtime follows its membership, and a refused subscription is not fatal 2026-10-04 00:14:04 +02:00
mesh-admin 895c2afad1 Merge pull request 'Issue 218: a mesh seat answered by a non-holder (located, fixed in mesh-controller#248)' (#339) from issues/218-a-mesh-seat-answered-by-a-non-holder into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 21:32:24 +00:00
jochen 4b8c5e3b11 Issue 218 located: the controller issued a mesh seat to every claimant; fixed in mesh-controller#248 2026-10-03 23:31:13 +02:00
mesh-admin d362155401 Merge pull request 'Issue 218: a mesh seat is answered by a module on a machine that does not hold it' (#338) from issues/218-a-mesh-seat-answered-by-a-non-holder into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 21:25:27 +00:00
jochen 557760e537 Issue 218: a mesh seat is answered by a module on a machine that does not hold it 2026-10-03 23:25:17 +02:00
mesh-admin 6b1ebd1d4a Merge pull request 'Issue 217: a refused announcement took down every container's runtime, and the console with it' (#336) from issues/217-a-refused-announcement into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 21:20:06 +00:00
jschoubben 8184585213 ADR 0199: a module that answers names declares its zone, and a node's hosts file is one module's
The per-node resolvers 0194 retires held two kinds of names that are neither nodes nor routes: the
lab's scenario machines and an operator's own lines. A zone a module declares is forwarded by the
mesh's resolver to that module; /etc/hosts is held per node through node-hosts-file, the operator's
lines in its kept region. Research 023 parks seats that define what their holder owns.
2026-10-03 23:19:36 +02:00
mesh-admin fa73a17ceb Merge pull request 'Issues 211, 214, 215, 216 diagnosed' (#335) from issues/211-214-216-diagnosed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 21:19:03 +00:00
jochen 08cea893bd Issue 217: a refused announcement took down every container's runtime, and the console with it 2026-10-03 22:47:28 +02:00
jochen 04625d3e35 Issues 211, 214, 215, 216 diagnosed: root causes and the branches that fix them 2026-10-03 22:28:40 +02:00
mesh-admin d7bb24b181 Merge pull request 'ADR 0198: a module's long-running code is launched by the node's runtime and reaches the bus through it' (#334) from decision/0198-a-modules-long-running-code-is-launched-by-the-runtime into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 20:21:04 +00:00
jochen 23d6e30b8a ADR 0198: a module's long-running code is launched by the node's runtime and reaches the bus through it; research 022; design 38 WP4c plan 2026-10-03 22:20:53 +02:00
mesh-admin 2043e90f35 Merge pull request 'Issues 214–216: a plan losing its own controller, a module pinned silently, a bundle never delivered' (#333) from issues/214-216-found-building-0192-0193 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 20:15:30 +00:00
jochen c9418af42e Issues 214–216: a plan losing its own controller, a module pinned silently, a bundle never delivered 2026-10-03 22:15:19 +02:00
mesh-admin 9c3e77999e Merge pull request 'Design 38 WP4d: every served bundle launched and node-tools in Go, proven live on all four machines' (#332) from design/38-wp4d-proven into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 20:07:30 +00:00
jochen 6943843fff Design 38 WP4d: every served bundle launched and node-tools in Go, proven live on all four machines 2026-10-03 22:07:20 +02:00
mesh-admin 34325d3566 Merge pull request 'ADR 0197: every tool announces itself on the bus, in the NATS services protocol' (#331) from decision/0196-every-tool-announces-itself into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 20:04:12 +00:00
jochen fa9e94d863 Renumber to ADR 0197: 0196 landed first on main 2026-10-03 22:03:58 +02:00
jochen 1b34821aa0 ADR 0196: every tool announces itself on the bus in the NATS services protocol 2026-10-03 22:03:34 +02:00
jschoubben 50ddaf8408 Merge pull request 'ADR 0196: a node asks the mesh's resolver first, and a public one only when it is silent' (#330) from decision/0196-nodes-ask-the-mesh-resolver-with-a-public-fallback into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 20:01:05 +00:00
jschoubben f5d518d256 ADR 0196: a node asks the mesh's resolver first, and a public one only when it is silent
ADR 0194 rejected sending every query to the mesh's resolver because a node with its tunnel down
would resolve nothing; a public resolver listed second answers exactly then. That drops the
systemd-resolved stub and the runtime's dns: containers copy the machine's resolvers. Narrows 0194;
amends connectivity §2.
2026-10-03 21:56:33 +02:00
jschoubben 577ddf0089 Merge pull request 'ADR 0194: the mesh has one resolver, and every node asks it for the mesh's names' (#326) from decision/0194-the-mesh-has-one-resolver into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:51:04 +00:00
jschoubben 13e28e6873 ADR 0194: the no-copies check allows each node's loopback stub 2026-10-03 21:51:03 +02:00
jschoubben 6b6ff76a19 ADR 0194: why every node needs a stub, and the systemd-resolved module that provides it 2026-10-03 21:50:33 +02:00
mesh-admin 8d5e6ef76f Merge pull request 'ADR 0195: the mesh's tools are found by address, not announced whole; research 021; to-be 34 §3a' (#329) from decision/0195-the-mesh-tools-are-found-by-address into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:49:37 +00:00
jochen 77813f4613 ADR 0195: the mesh's tools are found by address, not announced whole; research 021; to-be 34 §3a 2026-10-03 21:49:26 +02:00
mesh-admin 4ee8e3905d Merge pull request 'Issue 213: the controller is a Go program and still runs in a container' (#328) from issues/213-the-controller-runs-in-a-container into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:42:31 +00:00
jochen 216faec69e Issue 213: what the container gives it, measured 2026-10-03 21:42:22 +02:00
jochen e36b1a9e9c Issue 213: the controller is a Go program and still runs in a container 2026-10-03 21:42:11 +02:00
mesh-admin 5886969c75 Merge pull request 'Issue 212: a toolchain rebuild keeps the SDK it cached' (#327) from issues/212-a-toolchain-keeps-a-cached-sdk into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:24:49 +00:00
jochen 5fa43ff755 Issue 212: a toolchain rebuild keeps the SDK it cached 2026-10-03 21:24:39 +02:00
jschoubben 1de4a5f25e ADR 0194: the mesh has one resolver, and every node asks it for the mesh's names
Every resolution fault found on 2026-10-03 was a per-node copy disagreeing with the truth: a hosts
file read once, an operator's old line beside the mesh's, a node's resolver lent to a LAN. Every
tunnel already converges on one node. Retires node-dns-resolver for a mesh-scoped mesh-resolver;
nodes route only the mesh's suffix to it. Narrows 0121; amends connectivity §2 and the seats.
2026-10-03 21:21:49 +02:00
jschoubben 8a1fa37dce Merge pull request 'ADR 0191: domains are a node's — the resolver holds each node's internal domain, nothing else' (#323) from decision/0191-names-by-origin into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:12:26 +00:00
mesh-admin 44eb13acd0 Merge pull request 'ADR 0193: every bundle the runtime serves is launched, and the runtime knows no language; design 38 WP4d' (#325) from decision/0193-every-served-bundle-is-launched into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 19:05:12 +00:00
jochen 6d53f9168a ADR 0193: every bundle the runtime serves is launched, and the runtime knows no language; design 38 WP4d 2026-10-03 21:05:03 +02:00
mesh-admin 6a1fc71a4c Merge pull request 'Design 38 WP4b: four tools-only modules proven live from the runtime; two delivery traps' (#324) from design/38-wp4b-seven-proven into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 14:13:40 +00:00
jochen fb76fb7256 Design 38 WP4b: four tools-only modules proven live from the runtime; two delivery traps 2026-10-03 16:13:32 +02:00
jschoubben 2344bfb69b ADR 0191: domains are a node's — one internal, one or more public; the roster is the machines 2026-10-03 16:10:38 +02:00
jschoubben ca8a865e73 ADR 0191: the mesh's names are known by where they were composed, not by their suffix
A progressive insight: the rule and its check were stated as a suffix test; the mesh composes both
names of a route and publishes its internal one. The decision is unchanged.
2026-10-03 15:39:07 +02:00
mesh-admin 27c6881287 Merge pull request 'Issue 211: a bundle is built before the toolchain it is compiled in; ADR 0192 progressive insight; design 38 WP4b built, WP4c opened' (#322) from issues/211-and-0192-insight into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 13:36:43 +00:00
jochen f8458d6f2c Issue 211: a bundle is built before the toolchain it is compiled in; ADR 0192 progressive insight; design 38 WP4b built, WP4c opened 2026-10-03 15:36:26 +02:00
mesh-admin c5778f4366 Merge pull request 'ADR 0192: a tools bundle declares what it is given, and the runtime hands it to that bundle alone; research 020 graduated; design 38 WP4b' (#321) from decision/0192-what-a-bundled-tool-is-given into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 13:18:40 +00:00
jochen 4c1ad0ed45 ADR 0192: a tools bundle declares what it is given, and the runtime hands it to that bundle alone; research 020 graduated; design 38 WP4b 2026-10-03 15:18:22 +02:00
jschoubben 9873e951a9 Merge pull request 'ADR 0191: the mesh resolves only its own names; a public name resolves publicly' (#320) from decision/0191-the-mesh-resolves-only-its-own-domain into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 13:16:13 +00:00
jschoubben e5e6e56ecf ADR 0191: the mesh's resolver holds only the mesh's own names; a public name resolves publicly
Publishing every routed public name at a private address turned ace's LAN-facing resolver into an
outage for non-members: a phone got the control-node's tunnel address for the mail server. Routes
have internal names since 0151 and the proxy certifies public names publicly, so nothing needs the
private answer. Narrows 0066 and 0151; amends connectivity §2 and §5.
2026-10-03 15:11:45 +02:00
mesh-admin 65c30c576a Merge pull request 'Research 020: what a bundled tool is given; design 38 WP4: fail2ban followed, proven live' (#319) from research/020-what-a-bundled-tool-is-given into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 13:11:24 +00:00
jochen ee17cddb74 Research 020: what a bundled tool is given; design 38 WP4: fail2ban followed, proven live 2026-10-03 15:11:08 +02:00
mesh-admin 22e96e5bc7 Merge pull request 'Issue 210 resolved by mesh-host #80: an unchanged process keeps its record' (#318) from issues/210-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 11:56:30 +00:00
jochen 7e464e3b22 Issue 210 resolved by mesh-host #80: an unchanged process keeps its record 2026-10-03 13:44:49 +02:00
mesh-admin 502763ce5e Merge pull request 'Issue 209: a bundle's own SDK copy registers into a registry the runtime never reads; design 38 WP4 note' (#316) from issues/209-a-bundles-own-sdk-copy-registers-nowhere into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 11:20:38 +00:00
jochen eaae0e2b80 Merge remote-tracking branch 'origin/main' into issues/209-a-bundles-own-sdk-copy-registers-nowhere 2026-10-03 13:20:33 +02:00
mesh-admin 2ee6eab7b9 Merge pull request 'Issue 210: the host re-creates the node's runtime on every reconcile, restarting it every ten minutes' (#317) from issues/210-the-host-re-creates-the-runtime-every-cycle into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 11:20:27 +00:00
jochen ce5f85f65e Design 38 WP4 built and proven live on all four machines; issue 209 proven live 2026-10-03 13:20:12 +02:00
jochen 911125148c Issue 210: the record of the runtime's process carries no digest; the credential and the unit text are ruled out 2026-10-03 13:13:25 +02:00
jochen d718a917e5 Issue 210: the host re-creates the node's runtime on every reconcile, restarting it every ten minutes 2026-10-03 13:11:17 +02:00
jochen a572868333 Issue 209 resolved by mesh-tools #32 2026-10-03 13:07:41 +02:00
jochen 55443b67e6 Design 38 WP4: what the review of mesh-catalog #239 found — the credential goes, iptables is declared, escalation is an unchecked machine fact 2026-10-03 13:00:35 +02:00
jochen 89a202f12e Issue 209: a bundle's own SDK copy registers into a registry the runtime never reads; design 38 WP4 note
Found preparing WP4, before the first module's bundle was loaded beside the runtime's own:
proven with a two-copies probe, located in mesh-tools (node-tools), fixed by mesh-tools #32.
Design 38 WP4 records it and the two things the package left to the module — escalation
through sudo, and the filter file read from the manifest's path rather than a container env.
2026-10-03 12:46:51 +02:00
mesh-admin b342c9c3da Merge pull request 'Issues 203, 206 and 207 resolved by mesh-controller #233; 207's diagnosis, with its two questions left open' (#315) from issues/203-206-207-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 09:50:56 +00:00
jochen 336b8c9b62 Issues 203, 206 and 207 resolved by mesh-controller #233; 207's diagnosis, with its two questions left open 2026-10-03 11:50:20 +02:00
mesh-admin 0c82909845 Merge pull request 'Issues 204 and 205 resolved: mesh-controller #232 and mesh-host #79 merged' (#314) from issues/204-205-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 09:46:22 +00:00
jochen d8a0c1b02f Issues 204 and 205 resolved: mesh-controller #232 and mesh-host #79 merged 2026-10-03 11:45:19 +02:00
mesh-admin b232b44f4c Merge pull request 'Design 18 and ADR 0190: shared build work proven live on all four machines, and the five gaps the switch found' (#313) from design/18-shared-build-work-proven-live into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 09:44:19 +00:00
jochen c03f2cd4c6 Design 18 and ADR 0190: shared build work proven live on all four machines, and the five gaps the switch found 2026-10-03 11:43:53 +02:00
mesh-admin 73c4d24024 Merge pull request 'Issues 203–206 diagnosed and located' (#310) from issues/203-206-diagnosed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 09:43:00 +00:00
mesh-admin 3372d72da0 Merge pull request 'Issue 208: a seat's worker is made only when the controller starts, so a holder assigned later finds none' (#312) from issues/208-a-holder-assigned-later-finds-no-worker into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 09:33:28 +00:00
jochen 273b932329 Issue 208: a seat's worker is made only when the controller starts, so a holder assigned later finds none 2026-10-03 11:33:08 +02:00
mesh-admin f91a3efb6b Merge pull request 'Issue 207: a re-made worker replayed every ask the stream kept, and the mesh re-registered its past' (#311) from issues/207-a-re-made-worker-replayed-history into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 09:06:17 +00:00
jochen 2f0ce4ce19 Issue 207: a re-made worker replayed every ask the stream kept, and the mesh re-registered its past 2026-10-03 11:05:40 +02:00
jochen bf3338898e Issue 204: diagnosed and located — a declaration was numbered after composing, and a dying sender recorded nothing 2026-10-03 04:03:51 +02:00
jochen 4891cdeac5 Issues 203, 205, 206: diagnosed and located 2026-10-03 03:51:41 +02:00
mesh-admin 733cff4c3c Merge pull request 'Issue 206: a seat's worker changing type strands its holder, and the build that would fix it' (#309) from issues/206-a-worker-changing-type-under-its-holder into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 01:46:12 +00:00
jochen 561f26fb34 Issue 206: a seat's worker changing type strands its holder, and the build that would fix it 2026-10-03 03:45:33 +02:00
mesh-admin d8a58dadcf Merge pull request 'ADR 0190: a seat's work is shared by its holders, and building is the first such role; design 18 says where a build runs' (#306) from decision/0190-a-seats-work-is-shared-by-its-holders into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 00:44:27 +00:00
mesh-admin 3f4782cfb4 Merge pull request 'Design 38: WP3 proven live on all four machines; the three issues it found' (#308) from design/38-wp3-proven-live into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 00:41:47 +00:00
jochen d076647b5d Design 38: WP3 proven live on all four machines; the three issues it found 2026-10-03 02:41:24 +02:00
mesh-admin ae83c5e09b Merge pull request 'Issues 203–205: what the first node-tools rollout found' (#307) from issues/203-205-what-the-first-node-tools-rollout-found into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 00:41:07 +00:00
jochen feeea127e2 Issues 203–205: what the first node-tools rollout found — an unissued credential pushed, a stale declaration during a controller handover, a package resource against a stale database 2026-10-02 23:47:12 +02:00
jochen 76b563e0ce ADR 0190: the ack-wait race is issue 175 2026-10-02 22:29:53 +02:00
jochen f56686d1e5 ADR 0190: a seat's work is shared by its holders, and building is the first such role; design 18 says where a build runs
The build role was a mesh seat with one holder and its worker a push consumer with one delivery in
flight, so thirty-five images rebuilt serially on one machine while three others idled. The bus was
drawn for the alternative — a seat's accept subjects on a queue group of holders (design 25) — and
0190 uses it as one pattern for every role: a node seat with accepts, every holder pulling one ask
when idle, the asker addressing the role. Building is the first use: node-build-agent, held by the
build-agent module on every machine with a container runtime. Notes in 0121 and 0162 where their
facts went stale.
2026-10-02 22:29:08 +02:00
mesh-admin 5938d40dee Merge pull request 'ADRs 0164–0166 and issue 190: the container runtime gets a module, a seat and declared settings' (#271) from decision/docker-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 20:14:58 +00:00
mesh-admin f062672f83 Merge pull request 'Issues 192 and 193: the console reaches a person only by hand; the store's read-only query is not' (#272) from issues/192-193-console-registration-and-store-query into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 20:14:46 +00:00
jschoubben e4a0c73e2b Merge remote-tracking branch 'origin/main' into issues/192-193-console-registration-and-store-query 2026-10-02 22:14:24 +02:00
jschoubben d1aeee42a4 Regenerate the decision index after merging main 2026-10-02 22:14:21 +02:00
mesh-admin 81d780f973 Merge pull request 'Design 38: WP3 built — node-tools beside mesh-tools, the three things the plan did not say, and the gate refuses spreading not standing' (#304) from design/38-wp3-built-and-the-gate-softened into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 19:57:16 +00:00
jochen 3e30846e0f Design 38: point at ADR 0069's real file name 2026-10-02 21:46:34 +02:00
jochen b3f18c54c6 Design 38: WP3 built — node-tools beside mesh-tools, the three things the plan did not say, and the gate refuses spreading not standing 2026-10-02 21:46:01 +02:00
mesh-admin e11bf320c9 Merge pull request 'ADR 0188: a module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime; design 38 gains WP1b' (#300) from decision/0187-a-modules-own-code-is-bundles-in-any-language into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 19:27:39 +00:00
jschoubben da8b4b4ee4 Renumber to ADR 0188: 0187 landed on main first, as the dead-tracker record
Two records shared 0187 (issue 155's collision); the branch landing last
renumbers, and this is it. Only the number changes.
2026-10-02 21:01:02 +02:00
jschoubben c026d5221e Merge remote-tracking branch 'origin/main' into renumber-0187 2026-10-02 21:00:45 +02:00
mesh-admin 983fd412c6 Merge pull request 'ADRs 0180, 0186 and 0187: their live rows, done' (#302) from docs/the-four-fixes-proven-live into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 18:46:49 +00:00
jschoubben 9ac2493e2c ADRs 0180, 0186 and 0187: their live rows, done — all four machines filtered by the mesh alone, both front ends removed, no machine wrong or behind 2026-10-02 20:46:35 +02:00
mesh-admin 560f25c2c7 Merge pull request 'ADR 0187: a dead tracker is not the machine's failure' (#301) from fix/a-dead-tracker-is-not-the-meshs-failure into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 18:42:01 +00:00
jschoubben 9e0288128b ADR 0187: a dead tracker is not the machine's failure; design 32 2026-10-02 20:41:30 +02:00
jochen 709240ec1f ADR 0187: a module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime; design 38 gains WP1b
The operator's direction, absent from every record until now: the SDK must not limit who writes a
module; tools and services may be written in any language; one module may ship several bundles
(tools, a seat's implementation, a daemon); skeleton first, a full implementation when the work
requires it. ADR 0175 had the runtime import a bundle, which only JavaScript can be.

0187 makes a tools bundle a process the node's runtime launches and speaks MCP over stdio to —
the vocabulary the runtime already speaks outward — so any language with an MCP library can write
one today and the mesh's SDK per language is thin; the transport stays in the runtime (0039's
refusal, kept). Importing a TypeScript bundle is the shortcut, not the contract. Notes in 0175,
0039 and 0150 say where their mechanism moved; design 38 records WP1 as built and adds WP1b (the
launcher and the skeleton SDKs); the glossary's bundle widens.
2026-10-02 18:56:57 +02:00
mesh-admin d57289e049 Merge pull request 'ADR 0186: a ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang' (#299) from fix/a-ban-list-never-holds-a-neighbour into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 16:43:32 +00:00
jschoubben d4a2f99ab5 ADR 0186: a ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang; design 31 2026-10-02 18:42:16 +02:00
mesh-admin a9f91fdd0c Merge pull request 'ADRs 0184 and 0185: a service is still running a moment later; a control plane behind its row serves what it can' (#298) from fix/a-service-asked-to-run-is-still-running into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 16:25:44 +00:00
jschoubben 131a5e4714 Issue 201: what was established about the race while closing the outage half 2026-10-02 18:19:12 +02:00
jschoubben 329a24fdae ADRs 0184 and 0185: a service is still running a moment later; a control plane behind its row serves what it can; issue 201 half closed 2026-10-02 18:16:24 +02:00
mesh-admin 7f72f3b79a Merge pull request 'ADR 0179: the intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail' (#295) from feat/the-intrusion-seat-serves-its-verbs into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 15:28:48 +00:00
jschoubben 114a71f36f ADR 0179: built and proven live; the one fault the machine found, and the check that refuses it 2026-10-02 17:28:38 +02:00
jschoubben 0f407417f3 Merge main: the ufw record renumbered to 0180, and ADR 0175 retires the per-module tool runtime this one ships 2026-10-02 17:28:23 +02:00
mesh-admin 4af731df19 Merge pull request 'The operator's agent and its licence manager are modules: ADR 0181–0183, to-be 36 and 39' (#286) from feat/claude-code-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 15:23:38 +00:00
jochen 7b1dabbce0 The operator's agent and its licence manager are modules: ADR 0181–0183, to-be 36 and 39
The predecessor's agent module was retired and its six files stayed on both workstations telling
every session to use tools that no longer exist. This is its successor's design, revised during
review on the operator's directions: the host is module-agnostic, the controller has no part, and a
real licence manager hands out the correct licence in every situation.

- ADR 0181 (reconstructed): the operator account is a node fact stated by the operator; the home is
  derived unless stated; a resource may be placed under it owned by the account; a node with no
  account refuses one. What the controller shipped on 2026-09-27 without a record.
- ADR 0182: inside a home the module owns the directory and the files it places, writes into the
  tool's own files for its few keys, never declares a credential's content, and holds everything
  else as found; a predecessor's leftovers are the operator's to remove once.
- ADR 0183: claude-licence-manager holds the mesh seat anthropic-licence-manager and owns the
  Anthropic licences, grants (encrypted with a key the vault made for it), bindings per touchpoint,
  usage and audit; one rotation source under a lease; a token travels module to module sealed to
  each node's module key on request/reply, never as an event; the agent module alone writes what
  the agent reads; the host delivers package and state and knows nothing else. A bounded exception
  to ADR 0113; dated mechanism notes on ADR 0050 and 0113.
- To-be 36 (claude-code): the mesh's part of the agent's configuration lives in the agent's
  machine-wide managed directory, owned whole by the module and written by its code; nothing under
  the home but the credentials file of a subscription licence; the API-key licence through the
  key-helper; the console as a node-scoped provision (to-be 34 amended); MCP servers as settings
  with an mcp_configure tool; one agent directory per machine shared by every session.
- To-be 39 (claude-licence-manager): store, the two licence kinds, keeping a grant alive, the
  hand-over, who gets which licence with the predecessor's fallbacks, adoption with the identity
  guard, the seat's verbs.

Numbers taken across main and every open branch at the time of the merge; to-be 14, 29 and 34
carry dated notes; the glossary gains "operator account".
2026-10-02 17:22:12 +02:00
mesh-admin 2caa5e827b Merge pull request 'To-be 38: building the operator's machine as work packages; ADR 0175 collision renumbered to 0180' (#296) from feat/the-operators-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 15:20:38 +00:00
jochen 179fd7f83f To-be 38: building the operator's machine as work packages; ADR 0175 collision renumbered to 0180
The runtime work of design 37 broken down the way design 28 broke down the
bus: what exists measured (the host needs no change — a process and an
archive are what the runtime and a bundle are; the runtime does the job for
one module and must do it for a list), the order the dependencies allow, and
eight packages each ending at a proof on the live mesh — the lab skipped by
the operator's decision, ADR 0149 cited. WP1 the runtime serves many modules;
WP2 the controller composes one per node (a node principal, bundle archives,
the runtime's process, the gate); WP3 the runtime is a module and the console
its serving mode; WP4 the packet filter moves first; WP5 the shell on a
server; WP6 the service manager on a workstation; WP7–8 named and not broken
down.

Main carried two records numbered 0175 and cycle.py refused it: the one that
landed last (the found front end is uninstalled) takes 0180, the next free
across main and the open changes, with a dated note; design 08's citation
follows it; the index is regenerated.
2026-10-02 17:20:14 +02:00
jschoubben d0d5799884 Issue 201: a push recreated the controller at a digest older than the seat row its successor wrote 2026-10-02 17:15:22 +02:00
jschoubben 9ba4de5557 ADR 0179: the intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail; designs 31 and 33 2026-10-02 17:02:49 +02:00
mesh-admin e1203e5a43 Merge pull request 'Research 019: a warm twin of the running mesh' (#294) from jschoubben/research-019 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 14:59:44 +00:00
jschoubben 4ce967619a Research 019: a warm twin of the running mesh in the lab 2026-10-02 16:59:36 +02:00
mesh-admin 6df2cfecd6 Merge pull request 'Research 018 graduates: ADRs 0173–0177 and to-be 37, the operator's machine' (#293) from feat/the-operators-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 14:58:48 +00:00
jochen 73047501f6 ADR 0154: the controller seat gains a generic command verb (dated note, by ADR 0175) 2026-10-02 16:53:15 +02:00
jochen bf39baf104 Research 018 graduates: ADRs 0173–0177 and to-be 37, the operator's machine
Every configurable thing on a node is a module, the home included, and a
module is whatever it declares (0173, extending 0040). A node varies a module
only through a setting rendered into the file or a kept region, never an edit
(0174, extending 0011; issue 168 first). One tool runtime per node serves every
module's tools on the host side, never in a container; the console is its
serving mode, renamed node-tools (0175, extending 0150; 0047/0150/0152 carry
dated notes). The login shell is a node seat held by one shell module with
`execute` as its contract (0176). A unit may be user-scoped and the service
manager is a node seat held by systemd (0177).

To-be 37 is handed off in-progress to mesh-host, mesh-controller, mesh-tools
and mesh-catalog, with the build in order: the account on every node, the
runtime, zsh, systemd, then the graphical stack. To-be 29 keeps ~/.ssh and
points at 37; 33 §6 and 34 are amended; the glossary gains node tools, bundle,
kept region, installed/holding, and retires flavor.
2026-10-02 16:34:57 +02:00
jschoubben 17ca9a262b 0164: a setting names the file it lands in (issue 198's leak between one module's files); 190 notes the fourth machine now has the resolver 2026-10-02 12:23:28 +02:00
jschoubben 967c793eaa Merge remote-tracking branch 'origin/main' into decision/docker-module 2026-10-02 12:23:27 +02:00
jschoubben 27c1db8a86 Review of 0164-0166 and 190: the mesh's own setting words stay settable; changing runtime verbs are not the console's wildcard; migration steps 1-2 are one push; dnsmasq's dns key dates from 09-23 2026-10-02 00:48:06 +02:00
jschoubben 24aeb203f7 Issue 193 resolved: both readers live on every machine, checked by asking each copy who it is 2026-10-02 00:35:03 +02:00
jschoubben afbfd5f29d Issue 193: mssql's variable substitution and shell commands, proven and fixed by mesh-catalog PR 210 2026-10-02 00:25:48 +02:00
jschoubben 696957aa5e Issue 193: proven on a throwaway server, fixed for postgres by mesh-catalog PR 209; mssql has the same hole 2026-10-02 00:09:38 +02:00
jschoubben 3d54fcbb86 Merge remote-tracking branch 'origin/main' into decision/docker-module 2026-10-02 00:02:57 +02:00
jschoubben b13ef1be81 Issues 192 and 193: the console reaches a person only by hand; the store's read-only query is not
192: no provision says where the console is, its port was never assigned, and nothing
owns a person's agent configuration since the predecessor left. 193: the query verb wraps
the caller's text in a read-only transaction the text can end, and its rows come back
keyed by BEGIN.
2026-10-02 00:02:51 +02:00
jschoubben f1941304cc ADRs 0164-0166 and issue 190: the container runtime gets a module, a seat and declared settings
Proposed for the operator's review: settings declared with defaults and cost (0164),
container-runtime as a kernel capability (0165), node-container-runtime seat with the
host creating containers through its holder (0166), and the runtime's file written by
modules that are not its own (190).
2026-10-01 23:13:18 +02:00
771 changed files with 26385 additions and 27712 deletions
+7 -4
View File
@@ -23,10 +23,13 @@ and says they disagree.
## Amending the to-be layer
1. Write the decision record. If it reverses an earlier one, that record gets
`status: superseded` and `superseded-by:` — its text is never edited.
2. Edit the design; set `updated:` to today.
3. If this came from an issue, set that issue's `amended-design:`.
1. Propose the decision record in the knowledge base (`hq-graduate`, steps 2 and 3). If it reverses an
earlier one, it `supersedes` it, or `changes` the clauses it replaces — the earlier record's text is
never edited.
2. Edit the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:` to today.
3. If this came from an issue, give the issue the document in the issue tracker: `amended-design` on its
`issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind `design` before then. Never in the
report's frontmatter (ADR 0275).
## Amending the as-is layer
+1 -1
View File
@@ -18,7 +18,7 @@ Record it as **one memory file** in Claude's persistent memory directory for thi
path is given in the session's memory instructions), with `metadata.type: project`, plus a
one-line pointer in `MEMORY.md`.
**Not** in `04-ISSUES`, `01-RESEARCH` or anywhere else in the repository:
**Not** in `04-ISSUES`, the knowledge base, or anywhere else in the repository:
- A parked thought is not an issue or a research effort. Giving it a number asserts it has been
triaged, which is exactly what deferring says has not happened.
+13 -8
View File
@@ -23,14 +23,19 @@ result is not "nothing to learn" — it is the reason the next person repeats th
## Steps
1. Read `00-report.md`. Move `status:` to `diagnosing`.
2. Investigate. Write `01-diagnosis.md` in the same folder: the trail, **dated**, including
what was ruled out and how. Archaeology — which commit, which pull request, which date a
behaviour changed — is the most valuable content here.
3. When the owner is known, set `status: located` and fill `located-in:` with repositories or
modules from [`00-META/repos.md`](../../../00-META/repos.md).
4. On resolution: `status: resolved`, fill `fixed-by:`. If the root cause was a design gap, run
`hq-graduate` for the amendment and fill `amended-design:`.
1. Read the issue: `mesh_call issue-tracker.get` with its number, and `00-report.md` when it has a
folder. Before you start, move it to `diagnosing` with `issue-tracker.move` and `by`, your name and node as
who@node (such as `fix-399@laptop`). The tracker refuses it while another worker is current, naming them.
2. Investigate. The trail goes in `issue-tracker.note`, **dated** in its text, including what was
ruled out and how; for an issue with a folder, in `01-diagnosis.md` there as well. Archaeology —
which commit, which pull request, which date a behaviour changed — is the most valuable content.
3. When the owner is known, `issue-tracker.move` to `located` with `located-in`: entries of the
tracker's registered list of owners (repositories, and modules as `mesh-catalog modules/<module>`);
the tracker refuses an owner it does not know.
4. Before you start the fix: `issue-tracker.move` to `fixing` with `by`.
5. On resolution: `issue-tracker.move` to `resolved`, taken only from `fixing`, with `fixed-by`, and `replay` or `replay-none`
for a core issue. If the root cause was a design gap, run `hq-graduate` for the amendment and give
`amended-design`. Nothing of this is written in the report's frontmatter.
## Rules
+21 -11
View File
@@ -8,25 +8,34 @@ description: Use when a research effort in HQ concludes and becomes design, or w
Closes a research effort into a decision and a design, or amends an existing design.
**Authoritative playbook:**
[`00-META/process/02-graduation.md`](../../../00-META/process/02-graduation.md) — read
it; this skill adds only the scaffolding.
it; this skill adds only the scaffolding. Decision records and research efforts live whole in the
knowledge base (seat `knowledge-base`, ADR 0290), reached through the mesh MCP server; nothing of them is
written into this repository.
## Steps
1. **Check against GENESIS** — `mission.md`, `context.md`, `effect.md`. If the conclusion
conflicts, say which is wrong, in writing, before proceeding.
2. **Write the decision record** in `02-DECISIONS/`, next free number, frontmatter per
[`02-DECISIONS/README.md`](../../../02-DECISIONS/README.md). `reconstructed: false` — this one is being taken,
not recovered. Record the rejected options.
3. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`,
empty `code:`, today's `updated:`, and `decisions:` pointing at the record.
4. **Close the effort** — the research `00-overview.md` gets `status: graduated` and `became:`
pointing at both.
2. **Reserve the record's number.** `mesh_call knowledge-base.reserve {"kind": "decision", "for": "<title>"}`.
3. **Propose the decision record.** `mesh_call knowledge-base.propose` with the reserved `number`, `title`,
`topic` (one of the six in [`02-DECISIONS/README.md`](../../../02-DECISIONS/README.md)), `date`,
`deciders`, `clauses` (each `"**Lead.** the statement"`), `sections` (Context, Considered Options,
Consequences, How it is checked, References), `links` (`extends`, `rests-on`, `supersedes`, `changes` with
`"NNNN §N"`, `cites`), `homes` (the design's path), and `needs-operator: true` when the operator must
approve it. A refusal names every rule broken: fix them all and propose again. The knowledge base
reviews it itself; on `changes` revise and propose again with `why`. `knowledge-base.get` shows its
status, reviews and the operator's ask.
4. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`,
empty `code:`, today's `updated:`, and `decisions:` listing `"ADR NNNN"`.
5. **Close the effort** — `knowledge-base.propose` the effort again, whole, with `status: graduated` and
`became` links to the record (`"NNNN"`) and the design (its path).
## Amending an existing design
A design changes only through a decision. Write the record first; if it reverses an earlier
one, set that record's `status: superseded` and `superseded-by:` — **never edit its text**.
Then edit the design and set `updated:`.
A design changes only through a decision. Propose the record first; if it reverses an earlier one,
it `supersedes` it (or `changes` the clauses it replaces) — **never edit the earlier record's text**;
a correction of fact is a progressive insight (`propose` the accepted record with `insight`). Then edit
the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:`.
## When something ships
@@ -42,3 +51,4 @@ cannot be checked, it is `in-progress`.
- Do not move a to-be document into `00-as-is/`.
- Do not edit an as-is document to describe an intention.
- Do not change a decision record's meaning. Supersede it.
- Do not write a decision record, or a copy of one, into this repository.
+2 -2
View File
@@ -10,8 +10,8 @@ Hands a settled design to a code repository. **Authoritative playbook:**
## Steps
1. **Confirm it is settled.** `status: designed`, and every claim traceable to a record in
`02-DECISIONS/`. An open question in the text is a reason to run `hq-new-research`, not to build
1. **Confirm it is settled.** `status: designed`, and every claim traceable to a decision record in the knowledge base
(`knowledge-base.get` for each entry of its `decisions:`). An open question in the text is a reason to run `hq-new-research`, not to build
around it.
2. **Name the owner.** Set `code:` from
[`00-META/repos.md`](../../../00-META/repos.md). If the repository does not exist yet,
+28 -9
View File
@@ -21,27 +21,46 @@ Opens a numbered issue. **Authoritative playbook:**
there, this is not an issue — it is a lookup. If the answer is a general lesson, it belongs in
both.
## The number comes from the issue tracker
Every issue is opened in the issue tracker (seat `issue-tracker`, module `mesh-issues`, ADR 0275). **Never take a
number by reading `main` or the open pull requests** — that is how numbers collided. If the tracker cannot be
reached, take no number and say so.
Decide the shape first:
- **Design or governance issue** — its narrative belongs in hq: it gets a folder `04-ISSUES/NNN-short-name/`.
- **Anything else** — it lives wholly in the tracker, and nothing is written in hq.
## Steps
1. Next free number. Create `04-ISSUES/NNN-short-name/00-report.md` with:
1. Load `mesh_call` and `mesh_describe` (deferred tools of the `mesh` MCP server) and open the issue:
`mesh_call issue-tracker.open` with `title` (the symptom in one line), `report` (what was seen, what was expected,
how it was found — the evidence: what was run, what came back, when), optionally `located-in` when the owner is
already known, and — **only for an issue with a folder** — `record: short-name` (lower-case words joined by
hyphens, no number). The answer carries the number `NNN`.
2. **Issue wholly in the tracker:** done. Its trail goes in `issue-tracker.note`, its status changes through
`issue-tracker.move`, its links through `issue-tracker.link`.
3. **Issue with a folder:** on a branch off `origin/main`, create `04-ISSUES/NNN-short-name/00-report.md`, `NNN` being
exactly the tracker's number and `short-name` exactly the `record` passed:
```yaml
---
status: open
opened: YYYY-MM-DD
located-in: []
fixed-by:
amended-design:
---
```
2. Write the symptom **as observed**, in plain terms, with the evidence that it happened —
what was run, what came back, when.
3. Say why it matters beyond the instance. An issue that is only one occurrence is a knowledge
then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix
live in the tracker (ADR 0275), and `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`,
`amended-design:`, `replay:` or `replay-none:` in any report. Notes and links go to the
tracker.
4. Write the symptom **as observed**, in plain terms, with the evidence that it happened.
5. Say why it matters beyond the instance. An issue that is only one occurrence is a knowledge
base entry.
4. End with open questions rather than a proposed fix. Diagnosis is a separate step.
6. End with open questions rather than a proposed fix. Diagnosis is a separate step.
## Do not
- Do not name nodes, domains, addresses or paths. This repository is public.
- Do not guess the owner — `located-in:` is filled by diagnosis, not by opening.
- Do not renumber a folder or reuse a number: a number never moves.
+20 -21
View File
@@ -5,38 +5,37 @@ description: Use when starting a new research effort in HQ — an idea, technolo
# hq-new-research
Scaffolds a new research effort. **Authoritative playbook:**
Starts a new research effort in the knowledge base. **Authoritative playbook:**
[`00-META/process/01-research.md`](../../../00-META/process/01-research.md) — read it;
this skill only does the mechanical setup.
this skill only does the mechanical setup. A research effort lives whole in the knowledge base (seat
`knowledge-base`, ADR 0290); nothing of it is written into this repository.
## Steps
1. Find the next free number: list `01-RESEARCH/NNN-*`, take the highest plus one, zero-padded
to three digits.
2. Create `01-RESEARCH/NNN-descriptive-name/` (kebab-case from the topic).
3. Create `00-overview.md` in it with exactly this frontmatter, then a prose summary of **what** is
being investigated, **why**, and **what it touches**:
All through the mesh MCP server (`mesh_describe` gives each verb's arguments).
```yaml
---
status: active
initiated: YYYY-MM-DD
touches: []
became: []
---
```
4. Do the research in further documents in the same folder. Keep the summary current.
1. **Read what is known.** `mesh_call knowledge-base.ask {"question": "<the question in plain words>"}`;
read an effort or record it names with `knowledge-base.get` (`kind: research` for an effort,
`reasoning: true` for its documents).
2. **Reserve the number.** `mesh_call knowledge-base.reserve {"kind": "research", "for": "<title>"}`. The
number is spent; never pick one by hand.
3. **Propose the effort.** `mesh_call knowledge-base.propose` with `kind: research`, the reserved `number`,
`title`, `status: active`, `date` (today), `sections` (the documents in order, each `name` and `text`;
the first `00-overview.md`: **what** is investigated, **why**, and **what it touches**), and `links`
of kind `touches` for every record, effort, issue or design it touches (`cites` for one it only names).
4. **Do the research** in further documents of the same effort. Each change is `propose` again with the
effort whole and `why`. A refusal names every rule broken: fix them all and propose again. On review
`changes`, revise and propose again; `knowledge-base.get` shows the reviews.
## Do not
- Do not restate the status in prose. It lives in frontmatter, in one place.
- Do not fill `became:` while the effort is open — `hq-graduate` sets it at closure.
- Do not skip or reuse a sequence number.
- Do not restate the status in prose. It is the effort's `status`, in one place.
- Do not give a `became` link while the effort is open — `hq-graduate` gives it at closure.
- Do not write into `03-DESIGN` from an open effort.
- Do not write the effort, or a copy of it, into this repository.
- Do not name the mesh being observed. Evidence is required; identification is forbidden.
## Closing
An effort never just stops. It closes through `hq-graduate` as `graduated` or `abandoned`,
always with `became:` pointing at what it turned into. Nothing is deleted.
always with a `became` link to what it turned into. Nothing is deleted.
+22 -14
View File
@@ -5,7 +5,7 @@ description: Use when you need a cross-cutting view of where HQ stands — resea
# hq-status
Generates a cross-cutting view **from frontmatter**. This is a read-and-render skill, not a
Generates a cross-cutting view **from frontmatter, the knowledge base and the issue tracker**. This is a read-and-render skill, not a
workflow — HQ has **no central status file by design** (decision 35). Every view is
generated on demand and never written back to disk.
@@ -13,29 +13,37 @@ generated on demand and never written back to disk.
| Section | Files | Frontmatter |
|---|---|---|
| Research | `01-RESEARCH/NNN-*/00-overview.md` | `status` (`active` / `graduated` / `abandoned`), `initiated`, `touches`, `became` |
| Research | the knowledge base, not a file: `knowledge-base.ask`, and `knowledge-base.get` with `kind: research` and a number | `status` (`active` / `graduated` / `abandoned`), `date`, links (`became`, `touches`, `cites`) |
| Design | `03-DESIGN/**/*.md` (not READMEs) | `layer` (`as-is` / `to-be`), `status` (`designed` / `in-progress` / `implemented` / `abandoned`), `code`, `updated`, `decisions` |
| Decisions | `02-DECISIONS/NNNN-*.md` | `status` (`proposed` / `accepted` / `superseded`), `date`, `deciders`, `reconstructed`, `superseded-by`, `extends` |
| Issues | `04-ISSUES/NNN-*/00-report.md` | `status` (`open` / `diagnosing` / `located` / `resolved` / `wontfix`), `opened`, `located-in`, `fixed-by`, `amended-design` |
| Decisions | the knowledge base, not a file: `knowledge-base.holds` with a topic (the clauses in force, with their records), `knowledge-base.get` with a number, `knowledge-base.links` | `status` (`proposed` / `accepted` / `superseded`), `topic`, `date`, `deciders`, each clause's status, links both ways, what changed it since |
| Issues | the issue tracker, not a file: `mesh_call issue-tracker.list` (optionally `status`, `owner`, `phrase`) and `issue-tracker.get` | `status` (`open` / `diagnosing` / `located` / `fixing` / `resolved` / `wontfix`), `worker` and `stale` while `diagnosing` or `fixing`, `opened`, `located-in`, `fixed-by`, `amended-design`, `record` (the folder here, when it has one) |
## Steps
1. Read the frontmatter block from every file above — a grep across each tree is enough, no
need to load bodies.
2. Render what was asked as Markdown tables. Group and sort sensibly. The **ADR index** is one
of these views: records ordered by number, with title, date and status, and reconstructed
ones marked.
1. Read the designs' frontmatter (a grep across `03-DESIGN/` is enough, no need to load bodies), the
issues from the tracker, and the records from the knowledge base. An issue report carries no status;
where an issue stands is only in the tracker (ADR 0275). A decision record's or research effort's
status is only in the knowledge base (ADR 0290).
2. Render what was asked as Markdown tables. Group and sort sensibly. The **ADR index** by topic is
`knowledge-base.holds` with each topic of the reading order `02-DECISIONS/README.md` writes, in that
order, with a `bound` large enough for the topic (it says what it cut); it is the only place the list is
shown (ADR 0248). The knowledge base has no verb that lists every record by number: a view by number
is built from `get` per number, and says which numbers it read.
3. Flag anything inconsistent at the end, as flags — do not silently correct the render:
- a `graduated` or `abandoned` effort with an empty `became:`
- a `graduated` or `abandoned` effort with no `became` link
- an `implemented` design with an empty `code:`
- a `superseded` record with no `superseded-by:`
- a `superseded` record nothing `supersedes`
- an `open` or `diagnosing` issue with no activity
- a to-be design whose `decisions:` points at a record that does not exist
- a to-be design whose `decisions:` names a record `knowledge-base.get` does not find, or one that is
not `accepted`
- an as-is design whose claims are older than its `updated:` date suggests
## Do not
- **Do not write a status file.** Central status files are explicitly rejected. The view is
always generated, always ephemeral. This includes the ADR index — the hand-written one had
already drifted after a single addition, which is why it was removed.
- Do not infer status from prose. Trust only the frontmatter; if it is wrong, flag it.
already drifted after a single addition, which is why it was removed, and the generated one
written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which
is why that went too. `index.py` fails if the list is stored there again.
- Do not infer status from prose. Trust only a design's frontmatter, the tracker for an issue and the
knowledge base for a record; if one is wrong, flag it.
+3 -1
View File
@@ -27,7 +27,9 @@ the claim "HQ is the source" is checked.
3. Publish it to the knowledge base under the constitution slug, replacing the body.
4. **Read it back and verify the change is present.** A publish that reported success and did
nothing is exactly the failure class this repository exists to name — do not skip this.
5. Note the sync in the decision record's Consequences.
5. Note the sync in the decision record's Consequences (in the knowledge base: while the record is
proposed, `knowledge-base.propose` it again with the note and `why`; once it is accepted, `propose` it with
`insight`, saying what stood before).
## Do not
+1
View File
@@ -0,0 +1 @@
__pycache__/
+3 -3
View File
@@ -35,7 +35,7 @@ comparing it against the code rather than by anyone noticing:
- It described the pipeline as having a separate builder process and a build stage that
packages. Neither was true after 2026-08-04; the documents stayed stale until 2026-08-06
([ADR 0010](../02-DECISIONS/0010-delivery.md)).
(ADR 0010).
- It listed the mesh as spanning a fixed number of named machines, which is exactly the
content this repository cannot carry.
@@ -45,10 +45,10 @@ symlinks at all — the rule is not merely "only the installer may link", and a
elevating linking to a principle points the opposite way from where this is going.
What exists today is that the installer owns and reconciles every link
([ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)) — an as-is fact, recorded in
(ADR 0012) — an as-is fact, recorded in
[`03-DESIGN/00-as-is/05-runtime-and-installation.md`](../03-DESIGN/00-as-is/05-runtime-and-installation.md).
Centralising who may link narrowed the incident class; it did not close it. The intent is to
remove the mechanism, recorded as [ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md).
remove the mechanism, recorded as ADR 0012.
A founding document contradicting the direction of travel is precisely the failure this folder
exists to prevent.
+85 -28
View File
@@ -1,9 +1,12 @@
# Checks
```
python3 00-META/checks/records.py structure: links, citations, supersession, topics
python3 00-META/checks/index.py the reading order in 02-DECISIONS/README.md is current
python3 00-META/checks/index.py --write regenerate it
python3 00-META/checks/records.py structure: links, no record written here, citations in the form "ADR NNNN"
python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
python3 00-META/checks/cycle.py the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
```
Non-zero exit on any problem, so it can be a gate rather than a report.
@@ -20,29 +23,49 @@ indistinguishable from one that cannot.
| Check | Asserts | Found |
|---|---|---|
| `links` | every relative link resolves | — (run ad hoc during authoring; now permanent) |
| `rests-on` | `decisions:` and `extends:` name records that exist and are **accepted** | the class behind both incidents |
| `live-citation` | a governing document citing a **superseded** record names its replacement in the same paragraph | `01-to-be/README.md` citing ADR 0022 as live guidance |
| `supersession` | if A says it was superseded by B, B says it supersedes A | ADR 0012 never declared that it superseded 0011 |
| `numbering` | the number in the filename is the number in the heading | — |
| `topics` | every record names a topic the index knows | — |
| *(index.py)* | the written reading order matches what the records say | — |
| `no-records` | `02-DECISIONS/` and `01-RESEARCH/` hold their README and nothing else: a decision record or research effort lives in the knowledge base (ADR 0290 rule 9) | written with the change that removed the records (issue 426); it failed on a record file put back in `02-DECISIONS/` before it passed |
| `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed |
| `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed |
| *(index.py)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
| `status-vs-code` | a to-be document naming specific code is not still `designed` | **ten documents**, several with a *What was built* section, describing lab-proven code |
## Where the rules on records went
The decision records and research efforts left this repository for the knowledge base (ADR 0290, issue 426),
and the rules these checks held over them left with them. The knowledge base holds each as a rule of its own,
named in its refusal: `knowledge-base.propose` refuses a record that breaks one of the rules that judge one
record, naming every reason at once, and the four rules that judge the whole record set run after every
acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, one table-test case per rule).
| Rule here until issue 426 | Where it lives now |
|---|---|
| `records.py` `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` |
| `records.py` `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 |
| `records.py` `live-citation`, in a record | the knowledge base's `live-citation` |
| `records.py` `live-citation`, in `00-META/` and `03-DESIGN/01-to-be/` | **nowhere yet**, as above: the reviewer checks a cited record's status with `knowledge-base.get`; issue 428 |
| `records.py` `supersession` | the knowledge base's `supersedes-accepted` and `self-link`, and the whole-set rule `supersession` |
| `records.py` `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` |
| `records.py` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
| `records.py` `numbering` | the knowledge base's `title` and `reserved` |
| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order `index.py` still checks |
| `records.py` `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight |
| `index.py --print` | `knowledge-base.holds` with a topic |
| `cycle.py` research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` |
| `cycle.py` no two decision records share a number | the knowledge base's `reserved`, and the issue tracker's counter behind `knowledge-base.reserve` |
| `cycle.py` an accepted decision cited by nothing | the knowledge base's whole-set rule `cited`: a record accepted there is cited by another record or names a home (`homes` at propose) |
| `cycle.py` a deferral names the issue that tracks it | the knowledge base's `deferral` |
| `words.py` over research efforts from 2026-10-07, and `kept` in `words-allowed.md` | the knowledge base's `retired-word` and `daemon-line`, from this glossary |
## What is deliberately not checked
- **`02-DECISIONS/` and `01-RESEARCH/` may cite superseded records freely.** A decision record
discusses history; research records what was observed. Flagging those would produce noise on
correct documents, and a check that cries wolf gets suppressed — which costs more than not
having it.
- **`03-DESIGN/00-as-is/` may rest on a superseded record.** It describes what runs, and what
runs was built under whatever was decided at the time
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md):
*as-is describing a superseded decision is exactly what as-is is for*).
- **Whether a citation's prose is still true.** Only whether the record it points at is live.
A document can cite an accepted record and describe it wrongly, and nothing here notices.
So "governing" means `00-META/` and `03-DESIGN/01-to-be/` — the documents that tell somebody
what to do.
- **Whether a record a document cites exists, or is still accepted.** A checkout cannot read the knowledge
base, and a check that called the mesh would make every merge here depend on it. Until designs become the
knowledge base's own kind (issue 428), the reviewer asks `knowledge-base.get` for each record a change
cites in `00-META/` or `03-DESIGN/01-to-be/` (the documents that tell somebody what to do). An as-is
document may rest on a superseded record: it describes what runs, and what runs was built under whatever
was decided at the time (ADR 0006: *as-is describing a superseded decision is exactly what as-is is for*).
- **Whether a citation's prose is still true.** A document can cite an accepted record and describe it
wrongly, and nothing here notices.
## Adding a check
@@ -51,9 +74,43 @@ whose failure has never been observed is a guess about its own correctness.
## cycle.py
The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)):
a to-be design names a decision, an in-progress/implemented design names its owning code, a
located/fixed issue names its owner, a fixed/resolved issue says what fixed it, a graduated
research overview says what it became, and no two issue records share a number (issue 155 — the
number is how a record is cited, and `main` lags every open pull request, so two people reading it
allocate the same one). `python3 00-META/checks/cycle.py`
The development cycle, checked (ADR 0080):
every design parses, its `layer:` matches its folder and its `status:` is known; a to-be design names a
decision in `decisions:`; an in-progress or implemented design names its owning code; and no two issue
records share a number (issue 155 — the number is how a record is cited, and `main` lags every open pull
request, so two people reading it allocate the same one). An issue report carries the day it was `opened:`
and no status key — `status:`, `located-in:`, `fixed-by:`, `amended-design:`, `replay:`, `replay-none:` or
`resolved:` — because an issue's status lives in the issue tracker (ADR 0275); it failed on all 361 reports
that carried them before they were removed. The rules on an issue's status this check held until then — an
owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237)
— are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left
with them (the table above). `python3 00-META/checks/cycle.py`
## words.py
One word per thing, checked (ADR 0244).
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
renamed* lines, and fails on a retired word or a bare identifier in running prose — what is left once code,
quotations, struck-through text, link targets, comments and frontmatter are taken out — in `00-META/`,
`03-DESIGN/`, `AGENTS.md`, `README.md`, and issues opened from 2026-10-07. Decision records and research
efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It
also fails when a glossary head word heads two entries or is also retired.
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
of the tools' retired words (`retired-words`) with the glossary.
It also fails on a **daemon's line** (ADR 0272,
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked
for in the same documents, from 2026-10-09 for issues, in the document's own words — code blocks and
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
(`retired-lines`) with the list.
It failed on something real before it passed: 581 uses of retired words in 63 documents on its first run, besides the glossary itself and research 034 —
"the host" for the node-engine in 30 designs, "control plane" in 10, the glossary's own entry for the tool runner —
and two glossary contradictions (the "console", the deprecated broker's seat), fixed in the change that
added it. A retired word's plural (`s` or `es` on its last word) is matched as the word: until 2026-10-07
it was not, so "alerts", "rollouts" and "the hosts" (the node-engines) passed while their singulars
failed; the plural failed on 15 uses in 10 documents before it passed. A verb that reads like a plural
("a node hosts") is a finding too, and is reworded. Homonyms (*plan*, *gate*, *tier*, *ask*, *store*, *record*, *check*) are not checked by it: a
word list cannot tell one sense from another, so they are reviewed.
Binary file not shown.
+35 -88
View File
@@ -4,7 +4,7 @@
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules,
and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only
frontmatter, because status lives in frontmatter and nowhere else.
frontmatter, because a status this repository holds lives in frontmatter and nowhere else.
What is enforced:
@@ -12,20 +12,25 @@ What is enforced:
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
design without a decision -- and once `in-progress` or `implemented` it names
its owning code (`code:`) -- no development without a design that says where.
issues a known `status:`; once `located`, `located-in:` names the owner;
once `resolved`, `fixed-by:` says what fixed it (prose counts --
"nothing, the capability existed" is an answer). And no two records share a
number -- the number is how a record is cited.
research a known `status:`; a `graduated` overview says what it `became:`, and every
target it names exists.
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
frontmatter, a research overview, an issue report, a 00-META document, or another
record's extends/supersedes chain. A decision nothing points at is one nobody will
find by following pointers -- which is how records go stale in people's heads.
issues every report has frontmatter, and no two records share a number -- the number is
how a record is cited. An issue's status, owner, fix and replay live in the issue
tracker since the import of slice 3 (ADR 0275, to-be 53), where `issue-tracker.move`
refuses at the write what this check refused at merge until then: a status outside
the five, `located` without an owner, `resolved` without a fix, a core issue
resolved without its replay (ADR 0237), `wontfix` without a reason. A report
carries none of those keys, only the day it was `opened:` -- the tracker holds
them, and two places holding one fact drift. The keys the reports opened before
the import carried were removed once the import held them; the commit it read
is on every imported issue.
Deliberately NOT enforced: `resolved` issues may leave `located-in` empty (a symptom that
turned out not to be a defect has no owner), and as-is docs need no decisions (they
describe what exists, not what was decided).
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
rules this check held over them are the knowledge base's now: a research effort's status and what it
became, and a decision's duplicate number, are refused by `knowledge-base.propose`; an accepted decision
cited by nothing is the knowledge base's whole-set rule `cited`; a deferral that names no issue is its
rule `deferral`. README.md beside this file says where each went.
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
was decided).
python3 00-META/checks/cycle.py
"""
@@ -38,8 +43,11 @@ import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
ISSUE_STATUSES = {"open", "diagnosing", "located", "resolved", "wontfix"}
RESEARCH_STATUSES = {"active", "graduated", "abandoned"}
# The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275,
# slice 3 of to-be 53), and `issue-tracker.move` enforces the cycle's rules on them at the write. A report
# carrying one of them is a status written in two places, whenever it was opened.
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved")
def rel(path):
@@ -131,88 +139,27 @@ def main():
else:
seen[number] = name
# And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
# on main from two sessions within the hour, and every check passed.
seen_records = {}
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
name = os.path.basename(path)
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen_records:
bad(os.path.join("02-DECISIONS", name),
"is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
"free number across main AND every open pull request; the branch that lands last "
"renumbers" % (number, seen_records[number]))
else:
seen_records[number] = name
# Since the import of slice 3 (ADR 0275), an issue's status, owner, fix and replay live in the issue
# tracker, and its `move` refuses at the write what this check refused at merge. A report carries none
# of those keys.
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
status = front.get("status")
if status not in ISSUE_STATUSES:
bad(path, "status %r is not one of %s" % (status, sorted(ISSUE_STATUSES)))
if status in ("located", "resolved") and not listy(front, "located-in"):
bad(path, "status %s but located-in is empty" % status)
if status == "resolved" and not listy(front, "fixed-by"):
bad(path, "status %s but fixed-by says nothing" % status)
# ---- research ----------------------------------------------------------------------
for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
opened = str(front.get("opened") or "")
if not opened:
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
status = front.get("status")
if status not in RESEARCH_STATUSES:
bad(path, "status %r is not one of %s" % (status, sorted(RESEARCH_STATUSES)))
if status == "graduated":
became = listy(front, "became")
if not became:
bad(path, "graduated but became: names nothing")
for target in became:
if not os.path.exists(os.path.join(ROOT, target)):
bad(path, "became names %s, which does not exist" % target)
# ---- decisions -------------------------------------------------------------------
records = {}
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
front = frontmatter(path)
records[os.path.basename(path)] = (path, (front or {}).get("status"))
cited = set()
sources = (glob.glob(os.path.join(ROOT, "03-DESIGN", "*", "*.md"))
+ glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))
+ glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))
+ glob.glob(os.path.join(ROOT, "00-META", "**", "*.md"), recursive=True))
for path in sources:
text = open(path, encoding="utf-8").read()
if os.sep + "03-DESIGN" + os.sep in path:
# A design doc's governing citations live in frontmatter; a prose mention is
# commentary, not a home.
m = re.match(r"^---\n(.*?)\n---", text, re.S)
text = m.group(1) if m else ""
for m in re.finditer(r"([0-9]{4}-[^\s\)\],#]+\.md)", text):
cited.add(os.path.basename(m.group(1)))
for name in records:
front = frontmatter(records[name][0]) or {}
for key in ("extends", "supersedes", "superseded-by"):
v = front.get(key)
if isinstance(v, str) and v:
cited.add(os.path.basename(v))
for name, (path, status) in records.items():
if status == "accepted" and name not in cited:
bad(path, "an accepted decision nothing in the cycle cites -- give it a home in a "
"design doc's decisions:, a research became:, an issue, or 00-META")
carried = [key for key in TRACKER_KEYS if key in front]
if carried:
bad(path, "carries %s, but an issue's status, owner, fix and replay live in the issue tracker "
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
% ", ".join("`%s:`" % key for key in carried))
checked = (
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
+ len(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md")))
+ len(records)
)
if failures:
print("cycle: %d document(s) break the development cycle:" % len(failures))
+48 -82
View File
@@ -1,106 +1,72 @@
#!/usr/bin/env python3
"""Generate the decision index, and check the written one still matches.
"""The reading order of the decision records: its topics are written here, its records are not.
A number identifies a record and never changes, so the folder listing is creation order rather
than reading order. The index is what carries the path — and it is written rather than only
generated on demand, because a reader on a forge sees the folder and not a command.
A number identifies a record and never changes, so the reading order is not the order of the numbers.
It is the six topics `02-DECISIONS/README.md` writes down, in order, and each record's own topic. The
records live in the knowledge base (ADR 0290, issue 426), which reads these lines through the records
module and refuses a record whose topic is not one of them; the list of records under a topic is its
answer to `knowledge-base.holds` with that topic, never stored here (ADR 0248, hq issue 297).
The objection to a written index is that it drifts. That objection is answered by checking it
rather than by refusing to write one, which is `how-we-build` §5: a rule states how it is
checked.
python3 00-META/checks/index.py check
python3 00-META/checks/index.py --write regenerate it
python3 00-META/checks/index.py fail if it is stale
It fails when the README's reading order is missing or names a topic twice, and when the README stores
a list of records again — the index markers, or a line of a generated list. The last one is what keeps
the conflict of issue 297 from coming back. Whether every record's topic is one of the six is the
knowledge base's rule `topic`, judged when a record is proposed.
"""
import glob
import io
import os
import re
import sys
README = "02-DECISIONS/README.md"
START = "<!-- index:start -->"
END = "<!-- index:end -->"
# The reading order. Topics a record may belong to, in the order somebody would learn the system.
TOPICS = [
("the mesh", "What the mesh is"),
("the tiers", "Its tiers, from the bottom up"),
("what runs on it", "What runs on them, and how it gets there"),
("building it", "How it is built"),
("checking it", "How it is checked"),
("how we work", "How we work"),
# A topic in the README's reading order, e.g.
# 1. **What the mesh is** — `topic: the mesh`. What it is for, ...
# The knowledge base (mesh-knowledge, reference.go) reads the same line, so the form is a contract.
TOPIC_LINE = re.compile(r"^\d+\.\s+\*\*(?P<label>[^*]+)\*\*\s+—\s+`topic:\s*(?P<topic>[^`]+)`", re.M)
# What a stored list looks like: the old markers, or a line of the list index.py used to print.
STORED = [
(re.compile(r"<!--\s*index:(start|end)\s*-->"), "an index marker"),
(re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of a generated list"),
]
def field(text, name):
m = re.search(r'^%s:\s*(.+)$' % name, text, re.M)
return m.group(1).strip() if m else None
def records():
out = []
for path in sorted(glob.glob('02-DECISIONS/0*.md')):
text = io.open(path, encoding='utf-8').read()
heading = re.search(r'^# \d+\.\s*(.+)$', text, re.M)
out.append({
"file": os.path.basename(path),
"number": os.path.basename(path)[:4],
"title": heading.group(1).strip() if heading else "(no heading)",
"topic": field(text, "topic"),
"status": field(text, "status"),
})
return out
def render(rs):
known = {t for t, _ in TOPICS}
lines = [START, ""]
for topic, label in TOPICS:
rows = [r for r in rs if r["topic"] == topic]
if not rows:
continue
lines.append("### %s" % label)
lines.append("")
for r in rows:
mark = "" if r["status"] == "accepted" else " *(%s)*" % r["status"]
lines.append("- **%s** — [%s](%s)%s" % (r["number"], r["title"], r["file"], mark))
lines.append("")
stray = [r for r in rs if r["topic"] not in known]
if stray:
lines.append("### Unfiled")
lines.append("")
for r in stray:
lines.append("- **%s** — [%s](%s) — `topic:` is %r, which is not one of %s" % (
r["number"], r["title"], r["file"], r["topic"], ", ".join(sorted(known))))
lines.append("")
lines.append(END)
return "\n".join(lines)
def reading_order(text=None):
"""The topics, in order, as (topic, label) pairs, from the README."""
if text is None:
text = io.open(README, encoding='utf-8').read()
return [(m.group("topic").strip(), m.group("label").strip()) for m in TOPIC_LINE.finditer(text)]
def main():
if "--print" in sys.argv:
print("index: the records live in the knowledge base (ADR 0290). Read a topic's with "
"`knowledge-base.holds` and that topic, through the mesh MCP server.")
return 2
text = io.open(README, encoding='utf-8').read()
wanted = render(records())
topics = reading_order(text)
if START not in text or END not in text:
print("index: %s has no index markers (%s / %s)" % (README, START, END))
problems = []
if not topics:
problems.append("%s writes no reading order: no line of the form "
"'1. **Label** — `topic: name`. ...'; the knowledge base refuses every "
"record without it" % README)
names = [t for t, _ in topics]
if len(set(names)) != len(names):
problems.append("%s names a topic twice in its reading order" % README)
for pattern, what in STORED:
if pattern.search(text):
problems.append("%s stores a list of records (%s). The records live in the knowledge base, "
"and a topic's list is `knowledge-base.holds` with that topic" % (README, what))
for p in problems:
print("index: " + p)
if problems:
return 1
current = text[text.index(START):text.index(END) + len(END)]
if "--write" in sys.argv:
if current == wanted:
print("index: already current")
return 0
io.open(README, 'w', encoding='utf-8').write(text.replace(current, wanted, 1))
print("index: written")
return 0
if current != wanted:
print("index: %s is stale. Regenerate it:\n"
" python3 00-META/checks/index.py --write" % README)
return 1
print("index: current")
print("index: %d topics in the reading order, no stored list" % len(topics))
return 0
+69 -259
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Structural checks over HQ's own records.
"""Structural checks over HQ's own documents.
Every check here exists because the thing it checks for actually happened. See README.md
for which incident is behind which check. Run from the repository root:
@@ -7,6 +7,11 @@ for which incident is behind which check. Run from the repository root:
python3 00-META/checks/records.py
Exits non-zero if anything fails, so it can be a gate rather than a report.
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
rules that judged them here are the knowledge base's now: `propose` refuses a record that breaks one.
README.md beside this file says where each rule went. What stays here judges what this repository
still holds, and keeps the records from coming back.
"""
import os
@@ -15,14 +20,17 @@ import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Where a citation is *guidance* rather than history. A document here tells somebody what to
# do, so a link to a superseded record is an instruction to follow a withdrawn decision.
# 02-DECISIONS and 01-RESEARCH are deliberately absent: they record what was decided and what
# was observed, and both legitimately discuss superseded records at length.
GOVERNING = ("00-META/", "03-DESIGN/01-to-be/")
LINK = re.compile(r"\[[^\]]*\]\((?!https?:|mailto:)([^)]+)\)")
ADR_FILE = re.compile(r"^(\d{4})-")
# The two folders the records left. Each keeps a README that points at the knowledge base, and
# 02-DECISIONS/README.md also writes the reading order's topics, which the knowledge base reads.
RECORD_FOLDERS = ("02-DECISIONS", "01-RESEARCH")
POINTERS = {"02-DECISIONS/README.md", "01-RESEARCH/README.md"}
# A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
DECISION_ENTRY = re.compile(r"^ADR \d{4}$")
ADR_MENTION = re.compile(r"\bADRs?\s+(\d+)\b")
RESEARCH_MENTION = re.compile(r"\b[Rr]esearch\s+(\d+)\b")
def markdown_files():
@@ -45,7 +53,7 @@ def read(path):
def frontmatter(text):
"""Minimal frontmatter reader — enough for the fields these checks use.
Not a YAML parser on purpose: a dependency in a repository that has none, to read four
Not a YAML parser on purpose: a dependency in a repository that has none, to read a few
scalar fields and one list, would cost more than it returns.
"""
if not text.startswith("---\n"):
@@ -74,24 +82,11 @@ def frontmatter(text):
return fields
def load_records():
"""Every decision record, by its four-digit number."""
records = {}
folder = os.path.join(ROOT, "02-DECISIONS")
for name in sorted(os.listdir(folder)):
match = ADR_FILE.match(name)
if not name.endswith(".md") or not match:
continue
path = os.path.join(folder, name)
text = read(path)
records[match.group(1)] = {
"number": match.group(1),
"name": name,
"path": path,
"text": text,
"front": frontmatter(text),
}
return records
def unquote(value):
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
return value[1:-1]
return value
class Failures:
@@ -129,242 +124,62 @@ def check_links(failures):
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
def check_rests_on(failures, records):
"""A document's `decisions:` and a record's `extends:` must name a live record.
def check_no_records(failures):
"""No decision record or research effort is written into this repository again (ADR 0290 rule 9).
These are the load-bearing citations: the document declares that it rests on that
decision. Resting on a withdrawn one is the defect this whole check set exists for.
The knowledge base is the only place a record is read; a file here would be a second copy that
drifts from it, and nothing would read it. The folders keep their pointer READMEs and nothing else.
"""
for path in markdown_files():
front = frontmatter(read(path))
cited = list(front.get("decisions", []) or [])
extends = front.get("extends")
if isinstance(extends, str) and extends:
cited.append(extends)
for entry in cited:
match = ADR_FILE.match(os.path.basename(entry))
if not match:
failures.add("rests-on", rel(path), f"not a decision record: {entry}")
continue
number = match.group(1)
if number not in records:
failures.add("rests-on", rel(path), f"no such record: {entry}")
continue
status = records[number]["front"].get("status")
if status != "accepted":
# A proposed record may extend another proposed one. Decisions are drafted in
# chains -- 0059 extends 0057 while both await review -- and refusing that would
# mean either drafting out of order or marking records accepted to satisfy a
# check, which is the failure this repository already made once.
if frontmatter(read(path)).get("status") == "proposed":
continue
# An as-is document describes what runs, and what runs was built under
# whatever was decided at the time. ADR 0056: "as-is describing a superseded
# decision is exactly what as-is is for."
if rel(path).startswith("03-DESIGN/00-as-is/"):
continue
# A withdrawn record's citations are history. It instructs nobody -- every reader
# is sent to its superseder -- so what it was built on may itself be withdrawn.
# Refusing that would mean rewriting the lineage of a record whose reasoning is
# the thing the immutability rule protects.
if frontmatter(read(path)).get("status") == "superseded":
continue
# An extension that supersedes legitimately names what it replaced.
this = ADR_FILE.match(os.path.basename(path))
supersedes = records[number]["front"].get("superseded-by", "")
if this and supersedes and os.path.basename(path) in str(supersedes):
for folder in RECORD_FOLDERS:
base = os.path.join(ROOT, folder)
if not os.path.isdir(base):
failures.add("no-records", folder, "the folder is gone; it keeps a README that points at the "
"knowledge base (and, for 02-DECISIONS, the reading order the knowledge base reads)")
continue
for current, dirs, files in os.walk(base):
for name in sorted(files + [d + "/" for d in dirs]):
path = rel(os.path.join(current, name))
if path.rstrip("/") in POINTERS:
continue
failures.add(
"rests-on",
rel(path),
f"rests on ADR {number}, which is '{status}' — a document may not rest on a "
f"record that is not accepted",
"no-records",
path,
"a decision record or research effort lives in the knowledge base, never here: "
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)",
)
def check_live_citations(failures, records):
"""In a governing document, a link to a superseded record must name its replacement.
def check_decisions_entries(failures):
"""A design's `decisions:` names each record as "ADR NNNN".
The reader of a rule needs to know the rule was withdrawn, and needs somewhere to go.
Naming the superseder in the same paragraph is both, and it is what a person would
write anyway.
A to-be design rests on the records it names, and the knowledge base answers for a record by its
number; an entry in another form is a citation nothing can follow.
"""
superseded = {
number: record["front"].get("superseded-by", "")
for number, record in records.items()
if record["front"].get("status") == "superseded"
}
for path in markdown_files():
if not any(rel(path).startswith(prefix) for prefix in GOVERNING):
if not rel(path).startswith("03-DESIGN/"):
continue
for entry in frontmatter(read(path)).get("decisions", []) or []:
if not DECISION_ENTRY.match(unquote(entry)):
failures.add("decisions-form", rel(path),
f"decisions: entry {entry!r} is not 'ADR NNNN' (four digits)")
def check_mentions(failures):
"""In running text, a decision record is "ADR NNNN" (four digits) and a research effort "research NNN"
(three): the form the knowledge base and the issue tracker read a citation in. A code span is an example,
and is not read."""
for path in markdown_files():
text = read(path)
offset = 0
for paragraph in text.split("\n\n"):
line_no = text[:offset].count("\n") + 1
offset += len(paragraph) + 2
targets = LINK.findall(paragraph)
for target in targets:
match = ADR_FILE.match(os.path.basename(target.split("#")[0]))
if not match or match.group(1) not in superseded:
continue
number = match.group(1)
replacement = os.path.basename(str(superseded[number]))
if not replacement:
failures.add(
"live-citation",
f"{rel(path)}:{line_no}",
f"cites superseded ADR {number}, which names no superseder",
)
continue
if not any(replacement in t for t in targets):
failures.add(
"live-citation",
f"{rel(path)}:{line_no}",
f"cites superseded ADR {number} without naming its replacement "
f"({replacement}) in the same paragraph",
)
def check_supersession_symmetry(failures, records):
"""If A says it was superseded by B, B must say it supersedes A."""
for number, record in records.items():
front = record["front"]
status = front.get("status")
by = os.path.basename(str(front.get("superseded-by", "")))
if status == "superseded" and not by:
failures.add("supersession", rel(record["path"]), "marked superseded but names no superseder")
continue
if by and status != "superseded":
failures.add("supersession", rel(record["path"]), f"names a superseder but status is '{status}'")
if not by:
continue
match = ADR_FILE.match(by)
if not match or match.group(1) not in records:
failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}")
continue
other = records[match.group(1)]
# `supersedes:` may name one record or several. One decision replacing two is a real
# situation -- two records that built and refined the same wrong mechanism are withdrawn
# by the one record that removes it -- and a check that allows only one would force
# either a chain of pro-forma records or an unmarked supersession.
claimed = other["front"].get("supersedes", "")
if isinstance(claimed, str):
claimed = [claimed] if claimed else []
claims = [os.path.basename(str(entry)) for entry in claimed]
if record["name"] not in claims:
failures.add(
"supersession",
rel(other["path"]),
f"ADR {number} says this supersedes it; this record does not say so "
f"(supersedes: {', '.join(claims) or 'absent'})",
)
def check_topics(failures, records):
"""Every record names a topic the index knows.
The topic is what puts a record in the reading order, so a record without one — or with one
nobody defined — disappears from the index rather than appearing in the wrong place. That is
the quiet failure, so it is the one checked.
"""
known = {"the mesh", "the tiers", "what runs on it", "building it", "checking it",
"how we work"}
for number, record in sorted(records.items()):
topic = record["front"].get("topic")
if not topic:
failures.add("topics", rel(record["path"]),
"no topic, so it has no place in the reading order")
elif topic not in known:
failures.add("topics", rel(record["path"]),
"topic %r is not one of: %s" % (topic, ", ".join(sorted(known))))
def check_numbering(failures, records):
"""The number in the filename is the number in the heading."""
for number, record in records.items():
heading = re.search(r"^# (\d+)\.", record["text"], re.M)
if not heading:
failures.add("numbering", rel(record["path"]), "no '# N. Title' heading")
elif heading.group(1) != number.lstrip("0"):
failures.add(
"numbering",
rel(record["path"]),
f"filename says {number}, heading says {heading.group(1)}",
)
def check_progressive_insights(failures, records):
"""A correction made inside a record is marked and dated, or it is a silent rewrite.
A record may be corrected in place when a *fact* in it went stale and the decision still
stands (`02-DECISIONS/README.md`, "Progressive insight"). The whole safety of that allowance
is that the correction is legible in the record rather than only in a diff nobody reads, so
the form is what is checked here: every mention of an insight is the marker, the marker
carries an ISO date, and that date is not earlier than the decision's own — an insight
predating the decision it corrects is a copied marker, not a correction.
What this cannot check is an edit made with no marker at all. Nothing mechanical can; that
one is the reviewer's, reading the diff. The check keeps the *marked* path honest so that an
unmarked change stands out as the anomaly it is.
"""
phrase = re.compile(r"progressive insight", re.I)
# Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across
# newlines will happily join an unrelated `**` far above to the marker below, reporting the
# whole span between them. It did exactly that the first time this ran.
# Trailing words after the date are allowed — "— 2026-09-26, correcting the one above." — so
# an insight can say what it relates to. Only the date's presence and position are fixed.
marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})[^*\n]*\*\*")
loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*")
iso = re.compile(r"^\d{4}-\d{2}-\d{2}$")
for number, record in sorted(records.items()):
text = record["text"]
if not phrase.search(text):
continue
decided = str(record["front"].get("date", ""))
good = [(m.start(), m.end(), m.group(1)) for m in marker.finditer(text)]
for m in loose.finditer(text):
if any(s <= m.start() and m.end() <= e for s, e, _ in good):
continue
# A bold run carrying a link is discussing an insight — usually another record's —
# rather than marking one. A marker never needs to cite anything.
if "](" in m.group(0):
continue
failures.add("insights", rel(record["path"]),
"a progressive insight is not in the dated marked form "
"'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0))
for _, _, stamp in good:
if decided and iso.match(decided) and stamp < decided:
failures.add("insights", rel(record["path"]),
"a progressive insight dated %s predates the decision (%s)"
% (stamp, decided))
covered = [(s, e) for s, e, _ in good]
for m in phrase.finditer(text):
if any(s <= m.start() and m.end() <= e for s, e in covered):
continue
line = text.rfind("\n", 0, m.start()) + 1
end = text.find("\n", m.end())
whole = text[line:end if end != -1 else len(text)]
if whole.lstrip().startswith("#"):
continue
# A line that also carries a link is discussing the rule, not marking a correction:
# a marker never needs to cite anything, and a record that reasons about the policy
# must be able to name it. Bare prose with no citation is the informal marking this
# is here to catch.
if "](" in whole:
continue
if loose.search(text, line, text.find("\n", m.end()) + 1 or len(text)):
continue
failures.add("insights", rel(record["path"]),
"'progressive insight' appears unmarked; a correction is marked and "
"dated, or it is a silent rewrite")
for number, line in enumerate(text.split("\n"), 1):
line = re.sub(r"`[^`]*`", "", line)
for m in ADR_MENTION.finditer(line):
if len(m.group(1)) != 4:
failures.add("mention-form", f"{rel(path)}:{number}",
f"{m.group(0)!r}: a decision record is named 'ADR NNNN', four digits")
for m in RESEARCH_MENTION.finditer(line):
if len(m.group(1)) != 3:
failures.add("mention-form", f"{rel(path)}:{number}",
f"{m.group(0)!r}: a research effort is named 'research NNN', three digits")
def check_status_against_code(failures):
@@ -400,16 +215,11 @@ def check_status_against_code(failures):
def main():
failures = Failures()
records = load_records()
check_links(failures)
check_rests_on(failures, records)
check_live_citations(failures, records)
check_supersession_symmetry(failures, records)
check_numbering(failures, records)
check_topics(failures, records)
check_no_records(failures)
check_decisions_entries(failures)
check_mentions(failures)
check_status_against_code(failures)
check_progressive_insights(failures, records)
print(f"records: {len(records)} decision records checked")
return failures.report()
+10
View File
@@ -0,0 +1,10 @@
# Words allowed — documents `words.py` does not hold to the glossary's retired words
Read by [`words.py`](words.py) (ADR 0244).
One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words
themselves. A row whose document no longer uses a retired word fails too, so this list only shrinks. The
research efforts this list kept as written (research 034 and 042) live in the knowledge base now (ADR 0290),
which keeps a record's words as it was imported.
| Document | Until | Why |
|---|---|---|
+377
View File
@@ -0,0 +1,377 @@
#!/usr/bin/env python3
"""One word per thing, checked (ADR 0244).
The glossary (00-META/glossary.md) is the authority on the mesh's words. It names the words it
retired on one fixed kind of line, so a program can read them:
*Not:* ~~control plane~~, ~~overlay~~ (hq)
A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only;
`(tools)` in the descriptions of the mesh's tools only. And it names code that still carries an old
name until that code is renamed:
*Identifier until renamed:* `mesh-host` — the repository, binary and service unit
An identifier is allowed inside a code span and nowhere else.
What is enforced:
retired no retired word of scope `hq` (or none), and no identifier, in running prose of a
checked document. Running prose is what is left once code blocks, code spans, block
quotes, text in quotation marks, struck-through text, link targets, HTML comments and
frontmatter are taken out: a quotation keeps the words it quotes, a link target is a file
name, and an identifier lives in a code span. A word's plural is the word: "alerts" fails
as "alert" does.
unique no head word (a bolded word opening a glossary entry) heads two entries, and no head
word is also struck through on a *Not:* line.
tools list with `--list tools` it prints the words retired in the tools' descriptions, which is the
copy the catalogue's own check keeps (mesh-catalog `retired-words`). With
MESH_CATALOG_DIR set to a checkout of the catalogue it also fails when that copy differs.
lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09). The operator
types one command, `nox` (`mesh-cli`); the node-engine (`nox-mesh-host`) and the
controller (`mesh-controller`) are daemons, and a person never types their names — so a
document names a `nox` line, never `mesh-controller push` or `mesh-host hand-over`. The
lines are DAEMON_LINES below: each daemon's commands, and which of them are a line only
with an argument after them ("the mesh-controller module" is prose; `mesh-controller
module check …` is a line). A line is looked for in the document's own words — code
blocks and code spans included, since that is where a line to type is written — with
quotations, block quotes, link targets and comments taken out. `--list lines` prints the
list, which the catalogue's own check keeps a copy of (mesh-catalog `retired-lines`); with
MESH_CATALOG_DIR set it also fails when that copy differs. The node-engine's install-time
subcommands (`nox-mesh-host enrol`, `key`, `reconcile`, …) are not lines here: they are
typed at a machine no mesh reaches yet, and the engine's usage says so.
Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened
on or after FROM (on or after LINES_FROM for the lines). The decision records and research efforts live in
the knowledge base (ADR 0290, issue 426), which holds a record proposed there to the same words through its
rules `retired-word` and `daemon-line`, read from this glossary.
A document the check fails on that cannot be reworded in the change that found it is named, with a date
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
an entry for a document that no longer needs it fails too.
python3 00-META/checks/words.py
python3 00-META/checks/words.py --list tools
python3 00-META/checks/words.py --list lines
"""
import datetime
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
GLOSSARY = "00-META/glossary.md"
ALLOWED = "00-META/checks/words-allowed.md"
# The day the rule began (ADR 0244): issues opened from it on are held to it.
FROM = "2026-10-07"
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
# The daemons' lines (ADR 0272, its insight of 2026-10-09), and the day that rule began. "alone" is a
# command that is a line by itself; "with an argument" one that is a line only when written as one.
LINES_FROM = "2026-10-09"
DAEMON_LINES = {
"mesh-controller": {
"alone": ("adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act "
"hand-acts healers identity kill licence migrate operator pause pin plan prepare push rebuild "
"rehearse replay resume retire rollout rotate secret settings status token unassign unpin "
"upgrade").split(),
"with an argument": ("artifacts ask builds bus collection data delivery images mirrors module node "
"overlay plans queue seat seats take version").split(),
},
"mesh-host": {
"alone": "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version".split(),
"with an argument": [],
},
"nox-mesh-host": {"alone": ["hand-over"], "with an argument": []},
}
NOT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Not:\*(.*)$", re.M)
STRUCK = re.compile(r"~~([^~]+)~~(?:\s*\((hq|tools)\))?")
IDENT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$", re.M)
CODE_SPAN = re.compile(r"`([^`]+)`")
HEAD = re.compile(r"^\s*-\s+\*\*([^*]+)\*\*", re.M)
def rel(path):
return os.path.relpath(path, ROOT)
def read(path):
with open(os.path.join(ROOT, path), encoding="utf-8") as handle:
return handle.read()
def frontmatter_field(text, name):
if not text.startswith("---\n"):
return None
end = text.find("\n---", 4)
m = re.search(r"^%s:\s*(\S+)" % name, text[4:end], re.M)
return m.group(1) if m else None
def glossary():
"""The retired words with their scope, the identifiers, and the head words."""
text = read(GLOSSARY)
retired = []
for line in NOT_LINE.findall(text):
for word, scope in STRUCK.findall(line):
for one in word.split(" / "):
retired.append((one.strip(), scope or "all"))
identifiers = []
for line in IDENT_LINE.findall(text):
identifiers += [i.strip() for i in CODE_SPAN.findall(line)]
heads = []
for head in HEAD.findall(text):
heads += [h.strip() for h in head.split(" / ")]
return retired, identifiers, heads
def pattern(word):
"""A whole-word, case-insensitive match; a space in the word matches a space, a line break or a hyphen.
The plural is the word too: its last part may end in `s` or `es` ("alerts", "control planes"), since
a retired word does not come back by being counted. Anything else joined on is another word.
"""
parts = [re.escape(p) for p in word.split()]
return re.compile(r"(?i)(?<![\w-])" + r"[\s-]+".join(parts) + r"(?:e?s)?(?![\w-])")
def blank(match):
return re.sub(r"[^\n]", " ", match.group(0))
def prose(text, glossary_file=False):
"""The text with everything that is not running prose blanked, keeping offsets and line numbers."""
if text.startswith("---\n"):
end = text.find("\n---", 4)
if end != -1:
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
steps = [
r"(?ms)^\s*```.*?^\s*```", # code blocks
r"(?s)<!--.*?-->", # comments
r"(?m)^\s*>.*$", # block quotes
r"`[^`\n]+`", # code spans
r"\]\([^)\s]*\)", # link targets
r"~~[^~\n]+~~", # struck through: a word named as retired
r"\"[^\"\n]*\"", # "quoted"
r"\u201c[^\u201d]*\u201d", # curly double quotes
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
]
if glossary_file:
steps[:0] = [NOT_LINE.pattern, IDENT_LINE.pattern]
for step in steps:
text = re.sub(step, blank, text)
return text
def issues(since=FROM):
"""The issue reports opened on or after `since`, file by file."""
out = []
base = os.path.join(ROOT, "04-ISSUES")
if not os.path.isdir(base):
return out
for issue in sorted(os.listdir(base)):
head = os.path.join(base, issue, "00-report.md")
if not os.path.isfile(head):
continue
date = frontmatter_field(read(rel(head)), "opened")
if not date or date < since:
continue
for name in sorted(os.listdir(os.path.join(base, issue))):
if name.endswith(".md"):
out.append("04-ISSUES/%s/%s" % (issue, name))
return out
def checked_documents(since=FROM):
docs = []
for entry in ALWAYS:
path = os.path.join(ROOT, entry)
if os.path.isfile(path):
docs.append(entry)
continue
for base, dirs, files in os.walk(path):
dirs.sort()
for name in sorted(files):
if name.endswith(".md"):
docs.append(rel(os.path.join(base, name)))
return docs + issues(since)
def allowed():
"""words-allowed.md: one row per document — | `document` | until | why |.
`until` is a date, after which the allowance fails like the words themselves.
"""
out, problems = {}, []
if not os.path.isfile(os.path.join(ROOT, ALLOWED)):
return out, problems
today = datetime.date.today().isoformat()
for line in read(ALLOWED).splitlines():
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) != 3 or not cells[0].startswith("`"):
continue
doc, until, why = cells[0].strip("`"), cells[1], cells[2]
if not why:
problems.append("%s: the allowance for %s gives no reason" % (ALLOWED, doc))
if not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
problems.append("%s: the allowance for %s has no date" % (ALLOWED, doc))
continue
elif until < today:
problems.append("%s: the allowance for %s ran out on %s — reword it" % (ALLOWED, doc, until))
continue
out[doc] = until
return out, problems
def check_retired(retired, identifiers):
problems = []
allowance, problems_allowed = allowed()
problems += problems_allowed
words = [(w, pattern(w), "retired") for w, scope in retired if scope in ("all", "hq")]
words += [(i, pattern(i), "an identifier, allowed only in a code span") for i in identifiers]
used = set()
for doc in checked_documents():
text = prose(read(doc), glossary_file=(doc == GLOSSARY))
for word, rx, why in words:
for m in rx.finditer(text):
if doc in allowance:
used.add(doc)
continue
line = text.count("\n", 0, m.start()) + 1
problems.append("%s:%d: %r is %s (glossary)" % (doc, line, m.group(0).replace("\n", " "), why))
for doc in allowance:
if doc not in used:
problems.append("%s: %s no longer uses a retired word — remove its allowance" % (ALLOWED, doc))
return problems
def lines_list():
"""The daemons' lines as the catalogue's copy lists them: `<daemon> <command>`, ` …` when an argument must follow."""
out = []
for daemon, kinds in DAEMON_LINES.items():
out += ["%s %s" % (daemon, c) for c in kinds["alone"]]
out += ["%s %s …" % (daemon, c) for c in kinds["with an argument"]]
return sorted(out)
def line_pattern(daemon, command, with_argument):
"""The line whole: the daemon, white space, the command. One that is a line only with an argument is matched
as a line is written — at the start of a line or a code span, a prompt allowed, an argument after it."""
d, c = re.escape(daemon), re.escape(command)
if with_argument:
return re.compile(r"(?m)(?:^|`)[ \t]*(?:\$[ \t]*)?(" + d + r"\s+" + c + r"\s+[^\s`])")
return re.compile(r"(?<![\w-])" + d + r"\s+" + c + r"(?![\w-])")
def own_words(text):
"""The text with what is not the document's own words blanked: as prose(), but a code block and a code span
are kept, since a line to type is written in one."""
if text.startswith("---\n"):
end = text.find("\n---", 4)
if end != -1:
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
for step in (
r"(?s)<!--.*?-->", # comments
r"(?m)^\s*>.*$", # block quotes
r"\]\([^)\s]*\)", # link targets
r"~~[^~\n]+~~", # struck through
r"\"[^\"\n]*\"", # "quoted"
r"\u201c[^\u201d]*\u201d", # curly double quotes
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
):
text = re.sub(step, blank, text)
return text
def check_lines():
problems = []
patterns = []
for daemon, kinds in DAEMON_LINES.items():
patterns += [(daemon, c, line_pattern(daemon, c, False)) for c in kinds["alone"]]
patterns += [(daemon, c, line_pattern(daemon, c, True)) for c in kinds["with an argument"]]
for doc in checked_documents(LINES_FROM):
text = own_words(read(doc))
for daemon, command, rx in patterns:
for m in rx.finditer(text):
line = text.count("\n", 0, m.start()) + 1
problems.append("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)"
% (doc, line, daemon, command))
return problems
def check_unique(retired, heads):
problems = []
seen = {}
for head in heads:
key = head.lower()
if key in seen:
problems.append("%s: %r heads two entries" % (GLOSSARY, head))
seen[key] = True
for word, _ in retired:
if word.lower() in seen:
problems.append("%s: %r is a head word and also retired" % (GLOSSARY, word))
return problems
def tools_list(retired):
return sorted({w for w, scope in retired if scope in ("all", "tools")}, key=str.lower)
def check_copy(retired):
catalogue = os.environ.get("MESH_CATALOG_DIR")
if not catalogue:
print("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalogue's copy of the list was not read")
return []
path = os.path.join(catalogue, "retired-words")
try:
with open(path, encoding="utf-8") as handle:
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
except OSError as err:
return ["the catalogue's copy of the retired words cannot be read: %s" % err]
want = tools_list(retired)
problems = []
if sorted(copy, key=str.lower) != want:
problems.append("the catalogue's retired-words differs from the glossary: it should list %s" % ", ".join(want))
path = os.path.join(catalogue, "retired-lines")
try:
with open(path, encoding="utf-8") as handle:
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
except OSError as err:
return problems + ["the catalogue's copy of the daemons' lines cannot be read: %s" % err]
if sorted(copy) != lines_list():
problems.append("the catalogue's retired-lines differs from this check's list: regenerate it with "
"`python3 00-META/checks/words.py --list lines`")
return problems
def main(argv):
retired, identifiers, heads = glossary()
if argv[1:2] == ["--list"]:
scope = argv[2] if len(argv) > 2 else "tools"
if scope == "lines":
words = lines_list()
else:
words = tools_list(retired) if scope == "tools" else sorted({w for w, s in retired if s in ("all", scope)})
print("\n".join(words))
return 0
if not retired:
print("words: the glossary names no retired word on a *Not:* line — the check would pass on anything")
return 1
problems = check_unique(retired, heads) + check_retired(retired, identifiers) + check_lines() + check_copy(retired)
for p in problems:
print(p)
if problems:
print("words: %d problem(s)" % len(problems))
return 1
print("words: %d retired words and %d identifiers, none in running prose; %d daemons' lines, none written; "
"%d head words, each once" % (len(retired), len(identifiers), len(lines_list()), len(heads)))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+1 -1
View File
@@ -11,7 +11,7 @@ Imagine the mesh works as intended. What is different?
An agent says what it wants — from a terminal, a phone, a message — and the mesh takes it
from there. It works out which nodes are involved, does the work, and returns a
result. Nobody opens a console, recalls which node holds what, or follows a runbook
result. Nobody opens a terminal, recalls which node holds what, or follows a runbook
written months ago.
The interface is intent. The mesh handles the rest.
+708 -79
View File
@@ -1,97 +1,726 @@
# Glossary — the words this repository uses, and the ones it stopped using
# Glossary — the words of the mesh, by domain, and the ones it stopped using
One name per thing. This page is the authority; where an older record says something else, that
record is being superseded, not this page. It exists because the terms kept drifting in
conversation — control plane / controller / master / hub for one thing, substrate / foundation for
another — and a mesh you cannot name precisely is a mesh two people describe differently.
record keeps its words and this page says how to read them. It exists because the words kept drifting
in conversation — "control plane", "controller" and "master" for one thing, "substrate" and
"foundation" for another — and a mesh you cannot name precisely is a mesh two people describe differently. The rule and
its check are ADR 0244;
the domains are drawn in [to-be 49](../03-DESIGN/01-to-be/49-the-mesh-in-domains.md).
## The mesh and its machines
## How to read this page
- **node** — a machine in the mesh. There are 0..n of them, and each runs the host agent. A node is
just a machine that has joined; being one implies nothing about what it runs.
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one
per mesh. "control-node" is not a separate kind of machine — it is a node that additionally runs
the controller (and, today, the foundation). Lose it and the other nodes keep running what they
were last told; they simply cannot be told anything new.
- ~~master / slave~~, ~~hub / peer~~ — not used. The relationship is *controller and nodes*, and no
node is subordinate: a node applies declarations on its own and survives the control-node dying.
A **domain** is an area of the mesh that owns a set of concepts: inside it each concept has one word,
and the domain decides what that word means. Other domains use the word as it is defined here and never
change its meaning. Every word is defined once, in the domain that owns it; a section lists the words
it uses from other domains under *Uses*, with no second definition.
## What runs the mesh
Two fixed lines follow an entry where they apply, and `00-META/checks/words.py` reads them:
- **controller** — the component that decides what each node should be, holds the mesh's records,
and tells nodes over the broker. Replaces **"control plane"** (borrowed from networking's
control-plane/data-plane, and opaque here).
- **mesh-controller** — the module that runs the controller. It **claims** the `mesh-controller`
seat at mesh scope, which is what makes it singular. Replaces the module name **`mesh-control`**.
(The git repository has been renamed `mesh-control` -> `mesh-controller` on the forge; the module,
container and image it produces are `mesh-controller`.)
- **foundation** — the store and the broker, raised at genesis before any module system exists.
Replaces **"substrate"** (a biology metaphor that landed for no one). The foundation is not a
third thing beside the store and broker — it *is* those two, named together.
- **store** — the one postgres server. It holds the controller's own context databases
(`inventory`, `identity`, `licences` — a context owns its store, [ADR 0008](../02-DECISIONS/0008-a-context-owns-its-store.md))
and every module's own database. One server, many databases — never one shared "mesh database".
- *Not:* followed by struck-through words — the words this entry replaced. A struck word with no scope
is retired everywhere; *(hq)* retires it in this repository's prose only; *(tools)* in the
descriptions of the mesh's tools and seat verbs only. A retired word may be quoted, never used.
- *Identifier until renamed:* followed by a name in a code span — code that still carries an old name.
It may stand in a code span and nowhere else until the code is renamed, and then the line goes.
Nothing else on this page is struck through. A word that means different things in different domains
is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
## Words every domain uses
- **mesh** — the whole: the nodes, the modules assigned to them, the controller that decides and the
records it keeps. One mesh per operator; a second mesh is a second everything.
- **domain** — an area of the mesh that owns a set of concepts and the one word for each, as described
in the section above (ADR 0244).
ADR 0006 called seven of them the controller's *contexts*; they carry over as domains under the same
names, except *observability*, which is now **Health and repair**. Where a domain's records live in
the controller, the domain owns that store alone (ADR 0008).
*Context* in that sense is not used in new writing; the word stays ordinary English, so this is
checked by review, not by `words.py`.
- **machine** — any computer: hardware, a virtual machine, before, during or outside its membership of
the mesh. Prose about the computer itself — its disks, its kernel, the network it sits on, what was on
it before the mesh — says machine.
- **node** — a machine the mesh has adopted and owns. A machine becomes a node when it joins
(ADR 0004), and from then on it runs the node-engine
and is either **adopted** (the mesh holds what it found there as found) or **converged** (the mesh made
it what it is) — ADR 0100.
Prose about a member of the mesh says node. A node is just a machine that has joined; being one
implies nothing about what it runs.
- **module** — the unit the mesh assigns: one named thing, defined by its manifest, that a node runs —
a service, a container, files, packages, its own code — and the tools it serves
(ADR 0040). Everything configurable on a node is a module.
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a **closed
set** the mesh defines: a claim naming a seat outside the set is refused. A seat may **deliver a
provision**, and its holder is then the mesh's answer for it when several modules provide it
(ADR 0126). The set, with who holds each
seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)).
A seat carries **verbs** — the tools every holder must serve. A verb added to a held seat is first
promised as optional, then served, then required (ADR 0246).
A node seat may also name **the paths its holder owns**, such as `/etc/hosts`: a claim whose module does
not declare them is refused, and only a claimant declares them (ADR 0287).
- **operator** — the person who runs the mesh, and the one the mesh talks to.
*Not:* ~~master~~ (hq)
- **person** — any human, as against the mesh acting unattended: *a person's word* releases a held
delivery, *a person* deletes a consumer's data. The operator is one; the word says that a human, not
the mesh, acted.
## Module — what a module is and declares
**Purpose.** To say what one module is, in the one document every other domain reads.
**Recorded in** the catalogue, one manifest per module. **Upstream of** every other domain; its words
are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0040, 0126, 0174, 0188,
0207, 0236, 0262.
**Uses:** module, seat, provision (Provisioning), data class (Data), health (Health and repair).
- **manifest** — the file a module is defined by (`module.json`): what it is, what it provides and
requires, the seats it claims, the resources it declares, its tools, its data and its health. Say
manifest in prose, not the file's name.
*Not:* ~~module definition~~ (hq)
- **resource** — one thing a manifest declares a node must have: a file, a directory, a service, a
container, a package, a bundle, an archive. A module **declares** resources; the verb *declares* is
this domain's and means what a manifest states.
- **claim** — a module taking a spot on a seat: `claims: [{name, scope}]` in a manifest. A mesh-scoped
exclusive claim is how the mesh says "there is one of me". A foundation seat is named after the role
it guards: the `mesh-controller`, `postgres` and `nats` modules claim the `mesh-controller`,
`mesh-store` and `mesh-broker` seats (ADR 0079,
ADR 0116).
- **setting** — a value a manifest declares and an assignment gives, one of the two ways a node varies
a module (ADR 0174).
What a "flavor" once varied is a setting or a separate module.
*Not:* ~~flavor~~
- **preference** — a setting whose manifest gives it a default, because its value is true of the
software and only tuned per node: a font size, a width
(ADR 0262).
A setting with no default is the operator's own value: a domain, an identity.
*Not:* ~~tunable~~ (hq)
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
lines survive every send and are given back when the module goes (ADR 0174). The other way a node
varies a module.
- **bundle** — the artifact a module's own code is built into — its tools, a seat's implementation, a
daemon — in any language the mesh has a toolchain for; never an image. A **tools bundle** speaks MCP to
the tool runner. One module may declare several
(ADR 0188).
- **tool** — one capability a module serves through the tool runner, addressed `<module>.<tool>`. A
seat's **verb** is the same thing carried by a seat rather than a module.
- **mesh-sdk** — the library a module's own code is written against, including the harness that serves a
tools bundle to the tool runner. The harness is part of it, not a library of its own.
*Not:* ~~tools-sdk~~
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for every
one. A grant on the publish side and nothing else; a module that declares none calls nothing.
- **upgrade policy** — how a module's new builds reach its nodes: `roll` (one node first, judged, then
the rest), `together`, or `record` (register the build, send it nowhere)
(ADR 0236).
The controller's verb `upgrade` shows it.
## Core — the mesh's own machinery
**Purpose.** To keep the controller, the bus, the store and every node's engine running and agreed, so
every other domain has something to run on. **Recorded by** the controller and the store.
**Upstream of** every domain but Module; the others use the bus and the store as they are.
**Decided by** ADR 0005, 0006, 0067, 0106, 0141, 0175, 0227, 0229, 0289.
**Uses:** node, module, seat.
- **controller** — the component that decides what each node should be, holds the mesh's records, and
tells nodes over the bus. The relationship is *controller and nodes*, and no node is subordinate: a
node applies its declaration on its own and survives the control-node dying.
*Not:* ~~control plane~~, ~~slave~~ (hq), ~~mesh-control~~ (hq)
- **mesh-controller** — the module that runs the controller. It claims the `mesh-controller` seat at
mesh scope, which is what makes it singular.
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one per
mesh. It is not a separate kind of node — it is a node that additionally runs the controller (and,
today, the foundation). Lose it and the other nodes keep running what they were last told; they simply
cannot be told anything new.
- **node-engine** — the program on every node that applies what the controller declares: it receives
the node's declaration, writes the files, runs the services and containers, and reports what it did.
It is the engine, not a module: it owns no file's content, and every file it writes belongs to the
module that declared it. *Agent* is avoided because that word means a coding agent here. A record
written before the rename keeps the old name. "The host" is retired in this repository's prose only:
in the tools' descriptions it is also an SSH server's or a container runtime's own word, so a
description that means the node-engine by it is found by review.
*Not:* ~~host agent~~, ~~the host~~ (hq), ~~node host~~
*Identifier until renamed:* `mesh-host` — the repository, its binary and its service unit; the rename is
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
- **tool runner** — the one program on each node that loads every assigned module's tools bundle and
serves every tool and every held seat's verb on the subjects the memberships issue; the node-engine
supervises it as a process beside the services it runs, never a container
(ADR 0175,
which called it "node tools"). Its mode on loopback is the mesh MCP server.
*Not:* ~~node tools~~, ~~tool runtime~~ (hq)
*Identifier until renamed:* `node-tools` — the module, its unit and its bus account; the rename is
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
- **foundation** — the store and the bus, raised at genesis before any module system exists. The
foundation is not a third thing beside the store and the bus — it *is* those two, named together.
*Not:* ~~substrate~~
- **genesis** — raising the foundation and the controller on an empty mesh, by the one installation
done by hand (ADR 0067).
- **store** — the one postgres server. It holds the controller's own databases (`inventory`,
`identity`, `licences`, each owned by one domain, ADR 0008) and every module's own database. One server,
many databases — never one shared mesh database. Bare *store* means this and nothing else
(see [Homonyms](#homonyms)).
- **bus** — the mesh's own nervous system: NATS, one per mesh, carrying every link the mesh has —
control, declarations, builds, events, tool calls
([ADR 0106](../02-DECISIONS/0106-the-bus-is-nats.md)). A module reaches it by requiring
`mesh-bus` ([ADR 0128](../02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md)); one that
does not require it has no account on it. Held by the `mesh-broker` seat, which is named after
the *role* rather than the server, so the server can change without the seat doing so.
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It
keeps running as an **ordinary provider** of the `amqp` provision, for modules that need a
message broker of their own the way something needs a database
([ADR 0127](../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md))) — no seat, not foundation,
never raised at genesis, and a mesh that never installs it is complete.
control, declarations, builds, events, tool calls (ADR 0106).
A module reaches it by requiring `mesh-bus` (ADR 0128);
one that does not require it has no account on it. Held by the `mesh-broker` seat, which is named
after the role rather than the server, so the server can change without the seat doing so. The `nats`
module holds it.
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It keeps
running as an **ordinary provider** of the `amqp` provision, for modules that need a message broker
of their own the way something needs a database
(ADR 0131) — it claims no
seat, is not foundation, is never raised at genesis, and a mesh that never installs it is complete.
Say *the deprecated broker*, not *the compatibility broker* (it serves the mesh's own modules, not only
the predecessor's) and not *the AMQP broker* (naming it after a protocol invites describing the bus by
contrast with it, which is backwards).
- **layer** — one of the four levels the mesh is built in, from the bottom: the node-engine, the
foundation, the controller, the surfaces. The records' `topic: the tiers` and `repos.md` say *tier* for
this; new prose says layer (see [Homonyms](#homonyms)).
- **lease** and **epoch** — which controller may send (the lease, held in the bus and renewed), and the
order of what it sent (the epoch, which a node reads before it accepts a declaration)
(ADR 0229).
Say *the deprecated broker*, not "the compatibility broker" (it serves the mesh's own modules,
not only the predecessor's) and not "the AMQP broker" (naming it after a protocol invites
describing the bus by contrast with it, which is backwards: the bus is the mesh's nervous
system and this is a module).
- **record mirror** — a key-value bucket on the bus that the controller writes from one kind of its
records (nodes, assignments, walks, builds, settings), after each change commits in its store and only
when the value differs, with one event per write. The store stays the record; the self-check compares
the two and raises `mirror-drifted` on any difference
(ADR 0289). Shortened to *mirror* where the
text is about the controller's records; the builder's *mirrored* `upstream` image (Change and delivery) is
a different thing, a copy of an outside image in the artifact store.
## What the mesh stores and serves
## Placement — what runs on which node
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
the **artifact-store**, an OCI registry that holds every kind as content-addressed blobs
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
Every node pulls from it. An image is one kind of artifact, and a module is not an image.
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
**Purpose.** To decide, send and apply what each node runs, and to say why.
**Recorded by** the controller's `inventory` database. **Upstream of** Provisioning, Connectivity, Data
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261.
**Uses:** node, machine, module, seat, setting (Module), manifest (Module).
## How modules relate to the mesh
- **assignment** — a module put on a node, with the settings that node gives it.
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
controller when that build registers the module, or ended with why when it does not
(ADR 0261).
- **scope** — where a seat or a claim holds: `node`, `site` or `mesh`. `node` is also the scope's
value in a manifest.
- **capacity** — how many holders a seat takes. A capacity-1 seat is exclusive. A higher-capacity seat
is a **bench**: several holders coexist. A **replicated** bench has one holder per node, each on record
and each answering the same, like `mesh-dns-resolver`
(ADR 0223). A
**kinded** bench has holders that are different modules, each claiming one **kind**, and a verb's
subject carries the kind; `channel` and `intake` are the only ones (ADR 0234).
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
holding the seat its family declares; *holding* is being the one — the login shell, the display session
— on that node (ADR 0176).
The module that holds a seat is its **holder**.
- **depends on a seat** — a module needing a seat held on its node by some module, without holding it.
Derived from the resources it declares, never stated: a `service` depends on `node-service-manager`, a
`package` on `node-package-manager`, a `container` on `node-container-runtime`
(ADR 0207).
Not a claim: a module claims a seat it holds and declares resources.
- **declaration** — what the controller sends one node: every resource of every module assigned there,
composed. The noun is this domain's; what a manifest says is that it *declares* (Module), and a manifest
is never called a declaration. The controller's verb `plan` previews a node's declaration.
- **send** — the controller giving a node its declaration. The controller's verb `push` asks for a
send (of one node, or of every node); prose says send. A git push and a phone's push notification are
other things (see [Homonyms](#homonyms)).
- **apply** — the node-engine making its node match its declaration; **reconcile** is doing so again
until nothing differs.
- **operator account** — the login name of the person who works on a node, stated on the node record;
empty for a node nobody logs into. Everything the mesh places under a person's home is resolved
against this account's home and owned by it
(ADR 0181).
Not a name a manifest carries.
- **agent account** — the login every coding agent on a node runs as, when the node names one of its own;
stated on the node record at the controller's terminal, never by a verb or a setting. Empty means agents
run as the operator account. It cannot become root without a person, and the node-engine judges that on
every look (ADR 0266).
- **account group** — a group of the machine's group database that a module puts an account in, by
declaring the account with that group and nothing else of it. Several modules may each add one to the
same account; the mesh takes back only an account group it added
(ADR 0252).
Never *membership*, which is the bus's word.
## Provisioning — one module serving another
**Purpose.** To resolve which module serves a provision for which consumer, and to wire the two.
**Recorded by** the controller. **Upstream of** Data; downstream of Module, Placement and Identity.
**Decided by** ADR 0027, 0084, 0138, 0225, 0230, 0263.
**Uses:** module, seat, credential (Identity and access).
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a
**closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may
**deliver a provision**, and its holder is then the mesh's answer for it when several modules
provide it ([ADR 0126](../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md))).
The set, with who holds each seat, is the overview of what a mesh has
([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat has a **capacity**: a
capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders
coexist).
- **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
what makes a module *the* provider of it.
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat is a
role you occupy, and the two meet where a seat delivers a provision: occupying the seat is what makes
a module *the* provider of it.
- **provider** and **consumer** — the module that provides a provision, and the module that requires
it. Which of several providers serves a consumer is resolved
(ADR 0084).
- **pin** — a person's choice of provider for a consumer, which resolution then respects.
- **endpoint** — where a consumer reaches its provider; an assignment binds it and says how far it
reaches (ADR 0138).
- **retire** — a provider stops serving a consumer the mesh no longer asks for; what it kept is deleted
only by a person (ADR 0230).
- **share** — a folder one node offers to the mesh's other nodes over the private network: the provision
`nfs-share`, provided by the holder of the node seat `node-nfs-server`, one share per folder
(ADR 0263).
- **mount** — a share, or an occasional source, made visible at a path on one node by the holder of the
node seat `node-mounts`, as a mount unit and an automount unit, mounted on access (ADR 0263).
- **occasional source** — a share on a device that is not a node and comes and goes, such as a USB disk;
its absence is a state, *not present*, never a fault (ADR 0263).
## The surfaces
## Identity and access — who may do what
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
protocol, and the console is a module.
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
**Purpose.** To issue, hold, rotate and check the credentials and grants by which modules, nodes, agents
and the operator act. **Recorded by** the controller's `identity` and `licences` databases and the vault.
**Upstream of** Provisioning, Change and delivery, and Operator and conversation.
**Decided by** ADR 0085, 0113, 0183, 0225, 0234.
**Uses:** module, node, operator, consumer (Provisioning).
- **credential** — anything that proves an identity: a password, a key, a token. A **pair credential**
is the two ends of one credential, the consumer's and the provider's.
- **secret** — a credential or other value the vault keeps sealed and the node-engine unseals into a
file at apply (ADR 0085).
- **secret family** — a module's own secrets declared as one name ending in `-*`; each member is the prefix
and a name (`smb-password-<source>`, one per source the module's settings name), given by its full name
and never made by the mesh
(ADR 0283).
- **vault** — the module that owns every secret (`mesh-vault`,
[24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md)).
- **grant** — what an identity may call or reach, bounded by the provisions it requires
(ADR 0225).
- **bus account** and **membership** — a module's or a node's account on the bus, and the subjects it
is issued. The bus server calls its accounts *users*; the mesh says bus account.
- **rotate** — replacing a credential without a consumer holding one the provider does not know.
- **licence** — a model-access account the licence manager holds and **binds** to a consumer, the
coding agent on a node (ADR 0183).
Taking a consumer off a licence is **unbinding** it; the licence manager's verb for it is still
`release`, an identifier to rename.
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
verb, so a change that is the terminal's alone is taken (issue 339, issue 340) — or, since
ADR 0277,
proposed through a verb and set on the operator's warrant. In the controller's code it is a
line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no
agent can run as that account: the control-node, and a node whose agents run under an account of their own
(ADR 0272).
## Change and delivery — a commit on its way to the nodes
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276.
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`)
(ADR 0238).
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the
**builder**. Any node may hold the seat, so there is no build node by nature
(ADR 0237).
*Not:* ~~build machine~~
- **build queue** — the controller's queue of builds to run; one entry in it is a **build request**.
The queue's verbs still say *ask* for an entry; the conversation's ask is another thing
(see [Homonyms](#homonyms)).
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by **version**.
Served by the **package-registry** (gitea). Only a builder talks to it.
- **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a
mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the
**artifact store**, an OCI registry that holds every kind as content-addressed blobs
(ADR 0156).
An image is one kind of artifact, and a module is not an image. A package and an artifact are two
protocols, not one store being weak (ADR 0075).
- **build source** — the files one artifact is built from, in one or more repositories: for a Go program
its import closure (the directories of its packages, its Go module files) and its recipe; for any other
artifact the paths its manifest names, by default its module's own directory. Each artifact of a build
has its own, and a change rebuilds the artifacts whose build source it touches
(ADR 0267).
Older records (ADR 0015, ADR 0111) and designs say *build source* for the repository a module
is built from; this entry narrows it to the files within, and that use reads as *the repository holding
the build source*. Sharing a repository is not a dependency, and no build source is a whole repository
holding another module's files.
- **catalogue** — what the mesh holds of every module, at which commit; and the repository the
modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling.
*Not:* ~~catalog~~ (hq)
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
being announced to one of its final states: one pull request, one status, one note
(ADR 0239).
From its merge on it is delivered by the walk of the batch that answers it, and ends with that walk
(ADR 0276).
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
ADR 0250,
ADR 0270). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
mechanism, which the as-is designs describe; it is never a word for a delivery.
*Not:* ~~deployment~~ (hq)
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
whose walk a newer delivery took over before it ended; nor *delivered*, which its own walk or an empty plan
ends in (ADR 0270).
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
level: a group holds deliveries, never groups. Its state is derived from its members, never set.
- **delivery plan** — what a delivery does to the mesh: its **build plan** (the modules it moves and
their dependents, in tiers), its **deploy plan** (per node, what it receives and what waits for a
person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a
diffset; the controller's verb `delivery-plan` shows it. *Deploy* is used in this one place.
*Not:* ~~change plan~~, ~~release plan~~
- **merge window** — the rolling wait in which the controller collects merges before it plans them: each
merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest
10 minutes after its first merge (ADR 0276).
- **batch** — the merges one merge window collected, planned once as one walk. For a repository merged more
than once in it, the walk takes the latest commit, and the earlier merges are answered by it. Not *group*,
which is a delivery group.
- **mesh-delivery** — the module that owns deliveries and delivery groups, holding the mesh-scoped seat
of the same name. It records and decides; the controller sends, judges and rolls back when it is asked.
- **walk** — the controller's sending of one batch's builds across nodes, at most one trunk commit per
repository, tier by tier, one node first and judged at the first-node gate, then the rest (ADR 0236,
ADR 0276). One walk is open at a time; it names every merge it answers. A primitive the delivering stage
asks for, not an object anyone manages. A **tier** is one step of a walk: a module, then the modules
built against it. The controller's verb `plans` lists the walks. Walking is not a noun of its own:
a module *rolls out* by its `roll` policy.
*Not:* ~~rollout~~ (hq)
- **first-node gate** — the judgement on a delivery's first node: healthy three times, no witness put it
back, no condition raised since the send (ADR 0236 §2). Not *the gate* bare (see [Homonyms](#homonyms)).
- **core module** and **leaf module** — a core module is one on the controller's own path (the controller,
the node-engine, the tool runner, the bus, mesh-delivery) or the mesh's resolver, whose judgement protects
the mesh's ability to reach a node at all; a leaf module is every other module. Decided by
ADR 0282.
Not a *core component* (ADR 0236's controller, node-engine, tool runner and bus, each with a health probe):
the core modules add mesh-delivery and the resolver.
- **delivery time** — from a delivery's merge to every node of its deploy plan that is heard from running its
modules' builds and reporting them healthy. Its **delivery budget** is what it is held to: three minutes for
a leaf module (ADR 0304), ten for a core module (ADR 0282). Not the walk's time, which ends at the send. A
measure inside the fix budget, not the operator's target (ADR 0313).
- **fix time** — from an issue's first move to `fixing` to the moment the last of its fixes is delivered: the
operator's measure of a fix, the agent's work, the review and the checks included, the delivery time its last
part. Its **fix budget** is what it is held to: five minutes for a leaf module, ten for a core module, counted
from the move to `fixing`; an issue whose fixes move no module has none (ADR 0313). Not the delivery time,
which starts at the merge.
- **release** — a person's word that a held delivery goes on; the verb `mesh-delivery.release`. Used in
this sense only.
- **rollback** — putting a node back on the build it ran before, by something other than the build being
judged (a witness, ADR 0236).
- **the lab**, **scenario**, **replay** — a real mesh raised to run a change against before it reaches
nodes; what a lab run declares; and a recorded failure run again to prove a fix (ADR 0016, ADR 0237).
## Health and repair — what is wrong, and putting it right
**Purpose.** To notice what is wrong with the mesh, say it once, repair what may be repaired unattended,
and record what was done by hand. **Recorded by** the controller's condition store.
**Upstream of** Change and delivery (the first-node gate) and Operator and conversation (a condition
becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
**Uses:** node, module, node-engine (Core).
- **health** — a module's or a core component's statement of how it is alive and ready, which the
node-engine judges (ADR 0240).
- **relogin needed** — an account's health when the group database lists it in an account group and its
running session, started before, does not have that group yet. Said by the node-engine as the module's
resource of kind `account`; it clears at the first look after a new login (ADR 0252).
- **waiting** — a resource's health when its module's tool check says it waits for the operator: a named own
secret or setting of the module, not given yet, without which a part cannot work. Not a fault: the
controller checks it and says it as the condition `needs-operator`, naming the act, and the first-node gate
reads it as *waits for a person*
(ADR 0283).
- **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a
**finding**, before it becomes a condition.
- **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send),
and the watcher that raises a condition when it stops (to-be 45 §3).
- **condition** — one open fact about something the mesh owns that is wrong, with a key and a severity;
raised and cleared only by observation
(ADR 0231).
What a wrapped dashboard raises is its own; the mesh's notion is a condition.
*Not:* ~~alert~~ (hq)
- **self-check** — the controller's own examination of the mesh, run on demand and on a schedule
(to-be 45 §4). Its verb is `doctor`, an identifier; prose says self-check.
*Not:* ~~doctor~~ (hq)
- **healer** — a registered response to one condition kind: its **repair** (the ordinary path again),
its **budget**, its back-off and its brake. A healer may not withdraw, delete or recreate data.
- **drill** — something broken on purpose to see the mesh raise and clear its condition; a drill is
never counted as a repair.
- **rehearsal** — a question the operator starts at the controller's terminal (`rehearse`) and answers on a
channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the
operator's decision (ADR 0259).
Not a drill: nothing is broken.
- **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened.
- **witness** — what rolls a core component back when its new build does not become healthy: never the
component itself (to-be 45 §8).
## Data — what the mesh keeps, and getting it back
**Purpose.** To know every item of data a module holds, how precious it is, and to keep it recoverable.
**Recorded by** the manifests' `data` declarations and every node's `node-backup` seat.
**Upstream of** Change and delivery and Health and repair. **Decided by** ADR 0232, 0233, 0235.
**Uses:** module, node, provider and consumer (Provisioning), person.
- **data class** — how precious an item of data is, ranked by the operator: `irreplaceable`,
`valuable`, `rebuildable`, `cache`; and `none` for a provision that keeps nothing of anybody's
(ADR 0233).
- **backup** — a copy kept elsewhere by the node's `node-backup` holder; **restore point** — one backup
at one moment; **restore** — bringing it back beside the live data, never over it.
- **stream snapshot** — the bus's own backup of each stream, taken by the module holding `mesh-broker`
(ADR 0235).
- **sticky binding** — a consumer's tie to the data a provider keeps for it, which moves only by a
person (ADR 0232).
## Connectivity — how nodes and modules reach one another
**Purpose.** To make every node and module reachable by name where it should be, and unreachable where
it should not. **Recorded by** the controller. **Upstream of** Provisioning and Health and repair.
**Decided by** ADR 0007, 0117, 0138, 0223, 0226, 0247.
**Uses:** node, machine, assignment and endpoint.
- **private network** — the mesh's own encrypted network between its nodes, on which every node has an
address and a mesh name (ADR 0226).
The network the machine sits on without it is the **underlay**.
*Not:* ~~overlay~~ (hq)
- **anchor** and **hub** — the roles a node plays for the private network: the anchor is reachable from
outside and every node reaches it; a hub relays for nodes that cannot reach each other directly.
- **resolver** — a seat holder that answers the mesh's names; a node lists only the mesh's resolvers
(ADR 0223), or, where it holds one, its own resolver, which asks them. **uplink** — a node's
connection to the outside network, a seat (ADR 0117). **hostname** — a node's own name,
a seat.
- **split DNS** — resolving names by domain on one machine: a VPN's domains through the VPN's servers
over its link, every other name through the mesh's resolvers. The provision `split-dns`, provided by
the holder of the node seat `node-resolver`, the machine's **own resolver**, which exists only where
something requires it (ADR 0247).
- **proxy** and **public name** — the module that answers a public name and forwards it to an
endpoint on the private network.
- **packet filter** — what the mesh enforces on a node about which packets pass, the
`node-packet-filter` seat and its verb `rules`. A **found firewall** is a program the mesh found on a
machine and keeps retired or in force; *firewall* says that, and a wrapped program's firewall is its own.
- **intrusion prevention** and **ban** — refusing a source that misbehaved, and the refusal itself.
- **reach** — how far an assignment's endpoint may be reached from: the node, the private network, or
the outside (ADR 0138).
## Operator and conversation — the person the mesh works for
**Purpose.** To let the mesh and its operator talk: tell, ask, answer, and act only on an answer it can
trust. **Recorded by** the router (asks, warrants and the operator's identities).
**Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising
answer acts in whichever domain asked, through that domain's own verb; the conversation owns the
asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
**Uses:** operator, person, proof (Identity and access), release (Change and delivery).
- **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that
node reaches the mesh: the MCP server named `mesh`, with its five tools `mesh_overview`,
`mesh_machine`, `mesh_search`, `mesh_describe` and `mesh_call`. It is the tool runner's loopback mode,
not a module of its own. "Console" suggested a terminal or a shell, and the module `mesh-console` it
once named no longer exists; "the tool bridge" and "the brain" named the predecessor's program.
*Not:* ~~console~~ (hq), ~~mesh-console~~, ~~tool bridge~~
- **mesh-cli** — the module on every node that places the operator's command, and the command itself:
`/usr/local/bin/mesh-cli <command line>` asks the controller to run one of its command lines. It asks the
node-engine on its own machine, which says to the controller which account is asking; the controller runs the
line as the **controller's terminal** or as an ordinary call
(ADR 0272).
- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the
alias alone. It is the one command the operator types: every line the mesh addresses to the operator — a
condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's
or the node-engine's binary followed by a command), which `words.py` and the catalogue's check refuse
(ADR 0272,
its insight of 2026-10-09).
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from
the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one
holder of kind `desktop`) and not *bot* (that is one service's account).
- **router** — the module that holds the `operator-channel` seat, orders the channels and checks every
sender (ADR 0234).
- **headline / verdict / explanation / resolved line** — what a condition says to the operator in plain
words: a few words naming the thing and what is wrong; "Nothing for you to do." or "Needs you:" and the
one thing the operator can do themselves (its **needs**); one or two sentences on what happened and what
it means; the line said when it clears. Its **actions** are the answers a notification offers. Carried by
the condition beside its **summary**, which keeps the plans, commits and verbs for whoever looks closer
(ADR 0253).
The router may override "Nothing for you to do." in what it says, from the **tracking line** (ADR 0285).
- **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number,
date, acknowledge). An **authorising ask** is one whose chosen option its asker performs. The router
holds every ask, checks each answer's proofs and issues a **warrant**; the asker acts on it with its
own grants (ADR 0234, ADR 0259).
- **warrant** — the router's record that a person chose one option of one ask: the ask, the option,
who chose (the operator, as a channel kind's identity), through which channel, the proofs and when,
published for that asker alone. The only thing an asker acts on (ADR 0259).
- **proposal** — a change an agent, or anyone the bus admits, puts forward through a verb and that takes
effect only on the operator's warrant, bound to the exact thing shown: a shared instruction section (ADR
0274), a trusted setting's layer (ADR 0277).
A proposal changes nothing by itself; declined, expired or refused, it is discarded.
- **approval mark** — a tool's or verb's declaration in its manifest (`needs-approval`) that each call to it runs
only on the operator's warrant for that one call, with its exact arguments; the tool runner that serves it, or
the controller for its own verbs, asks and checks
(ADR 0286).
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
against its own list of the operator's identities: a **trusted** one is an operator message,
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
participant, which answers from read verbs); anything else is **untrusted input** — data, never
instructions.
- **reference** — an opaque token in a message's words standing for a detail the content rule keeps out
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
- **agent** — a coding agent: the operator's, or one the mesh runs. Not the node-engine, and not the
build seat's holder, whose seat name `node-build-agent` is an identifier to rename.
- **instruction section** — one named section of the managed instruction file, which every agent session on the
nodes it is for reads first, as the mesh's word: set at the controller's terminal in the agent module's
settings, or **proposed** by an agent and made active on each node only by the operator's warrant, which that node
proves itself (ADR 0274). A hand-over between sessions is one.
- **personal rule** — one short text in the operator's own words about how the operator wants their agent
to work with them, registered for the operator (the **operator scope** of the agent module) and rendered
into the agent's home on every node that names an operator account. Not a mesh-wide instruction section,
which is the mesh's word about the mesh, set at the controller's terminal
(ADR 0273).
- **lesson** — an agent's proposal, recorded when the operator expresses frustration or corrects a way of
working: the trigger (the operator's words), the cause (what the agent did) and the rule that would
prevent it. A lesson is `proposed`, `accepted` or `declined`; an accepted lesson is a personal rule, and a
proposed one is never read as one (ADR 0273).
## Media — what a person watches, listens to and reads
**Purpose.** To find, fetch and file copies of the works a person watches, listens to and reads, each in
the library its kind names. **Recorded by** the `media-importer` module's settings and its memory of what
it was told, and the servarr apps' own libraries. **Upstream of** nothing. **Decided by** ADR 0295, 0296,
0302.
**Uses:** module, seat, setting (Module).
- **content** — a work a person watches, listens to or reads: a film, an album and its tracks, an
episode, a book, a session of a sport event. It is known by an identity from a catalogue outside the
mesh, never by a file's name, and a work with no copy anywhere is still content (ADR 0302).
- **kind** — a kind of content, declared as configuration, never code: its parts, the metadata fields
that identify a work, the file extensions of its copies, and its folder and file templates. The first
are music, movie, series, audiobook, ebook and sport event.
- **media release** — one copy of a work an engine can fetch, such as an NZB or a video. Downloaded, it
sits in the downloads folder, not yet filed. ADR 0302 says *release*.
- **library** — one folder of one kind, filed by one importer: the media-importer's, named in its
settings, or a servarr app's. Several libraries may hold one kind, as TV Shows and Anime both hold
series.
- **media file** — a file on disk together with its metadata: its kind, the fields that identify the
work, and what part of the work the file is. It is what the media-importer takes.
- **media-importer** — the module that files a media file into its library at the place its kind's
templates make of its metadata. It files only the downloads it was told about, and never into a
servarr app's library (ADR 0302).
- **download client** — a module that fetches a media release into the downloads folder and emits
`download.added` and `download.completed` naming the download's id, its category and the path of its
files: `nzbget`, `qbittorrent`, `youtube`.
- **engine** — a module that searches for media releases: the holder of `usenet-search` (`nzbgeek`,
ADR 0295) or of `youtube-search` (`youtube`, ADR 0296).
- **grab** — an engine handing one media release to a download client. It names the work it is for, so
the media-importer knows where the copy belongs.
## The record — how this repository works
Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
**Recorded in** this repository, the issue tracker and the knowledge base. **Decided by** ADR 0019, 0080, 0244, 0275,
0284, 0290, 0291.
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation, held whole in the
knowledge base and named "research NNN";
its closing into a decision and a design; a design given to a code repository; a documented workflow
in `00-META/process/`.
- **decision record** — one numbered record in the knowledge base, named "ADR NNNN", never rewritten:
**superseded** by a later record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
*record* has other meanings (see [Homonyms](#homonyms)).
- **clause** — one numbered statement a decision record makes, with its own status (`proposed`, `in force`,
`changed`, `superseded`): a later record may change one clause and leave the rest
(ADR 0290). Not a *rule*, which is a line of
`00-META/how-we-build.md`.
- **knowledge base** — the role that holds the decision records and research efforts whole (number, status, topic,
typed links, clauses and reasoning, with every version), takes them as proposals, has them reviewed and accepted,
and answers what holds: the seat `knowledge-base`, held by the module `mesh-knowledge`
([55 — The knowledge base](../03-DESIGN/01-to-be/55-the-knowledge-base.md)). Not **the record**, the literal reader
of this repository.
- **design** — a document in `03-DESIGN/`: **as-is** (what runs) or **to-be** (what is being built).
- **issue** — a question about the mesh that someone must answer, numbered and tracked by the issue tracker, which
holds it whole: its report, diagnosis and resolution included (ADR 0291);
the folders in `04-ISSUES/` are imported there and removed.
An **incident** is a past event that taught a rule.
- **issue tracker** — the role that holds the mesh's issues and hands out their numbers: the seat `issue-tracker`,
held by the module `mesh-issues` ([53 — The issue tracker](../03-DESIGN/01-to-be/53-the-issue-tracker.md)).
Not hits's *item*; bare *ticket* is a homonym (see [Homonyms](#homonyms)).
*Not:* ~~ticketing system~~ (hq), ~~ticket system~~ (hq)
- **duplicate check** — the issue tracker's judgement, when an issue is opened, of whether it asks a question an
older issue not resolved already asks; a certain duplicate is closed `wontfix` with its report kept on the older
issue (ADR 0278).
Its pass over the backlog is the verb `groom`.
- **triage** — the issue tracker's judgement of an opened issue, made by the agent with no tools: whether it is a
duplicate (the **duplicate check**), its **severity** and what it **needs**, a suggested owner, and the issues it
depends on or relates to; it fills only what nobody gave (ADR 0279).
- **ready** — of an issue: every issue it depends on is wontfix, or resolved with its fix delivered.
- **worker** — of an issue: the agent or person, with its node (who@node), that moved it to `diagnosing` or `fixing`
and works on it; another is refused while the worker is current, unless it gives a reason to **take it over**. A
worker with nothing done on the issue for a day is **stale**. **Fixing** is the status an issue is at while its fix
is worked on; an issue is resolved only from it (ADR 0284).
Not a *claim* or a *holder*, which are a module's on a seat.
- **abandoned** — of an issue: at `diagnosing` or `fixing`, its worker stale and no pull request of it open; another
worker takes it over without a reason (ADR 0294).
Not a *stale claim*: a claim is a module's on a seat.
- **fix** — of an issue: a pull request its `fixed-by` names or it links; one closed without merging is
**withdrawn**. An issue at `fixing` with every fix delivered is **delivered, not resolved** (ADR 0294).
- **tracks** — of an issue and a condition: the issue links the condition's key with a link of kind `condition`. The
**tracking line** is the line the router says in every message about a condition: the issues that track it and
where each stands, or that none does (ADR 0285).
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
## Homonyms
A word below means different things in different domains. In a governing document it is never bare;
it is always the qualified form. **Checked by review, not by `words.py`:** a word list cannot tell one
sense from another, so the reviewer looks for the bare word in the diff. Where a homonym is settled by
renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| Word | Sense | Say | Domain |
|---|---|---|---|
| plan | what the controller would send one node | that node's **declaration** (verb `plan`) | Placement |
| | what a delivery would do to the mesh | **delivery plan** | Change and delivery |
| | the sending of one batch of commits across nodes | **walk** (verb `plans`) | Change and delivery |
| | a step a person starts, like the bus's | **planned step** | Core |
| membership | the subjects a bus account is issued | **membership** | Provisioning |
| | an account in a group of the machine | **account group** | Placement |
| push | the controller giving nodes their declarations | **send** (verb `push`) | Placement |
| | a git push | **git push** | The record |
| | a phone notification | **push notification** | Operator and conversation |
| release | a person letting a held delivery go on | **release** | Change and delivery |
| | taking a consumer off a licence | **unbind** (verb `release`, to rename) | Identity and access |
| | a copy of a work an engine can fetch | **media release** | Media |
| gate | the judgement on a delivery's first node | **first-node gate** | Change and delivery |
| | the pull request status | **merge gate** | Change and delivery |
| | a failed step stopping its module's later steps (ADR 0136) | *a failed step holds its module* | Placement |
| tier | a level of the mesh | **layer** | Core |
| | a step of a walk | **tier** | Change and delivery |
| | how much proof an ask needs | **assurance level** | Identity and access |
| ask | a request for the operator's input | **ask** | Operator and conversation |
| | an entry in the build queue | **build request** | Change and delivery |
| store | the one database server | **store** | Core |
| | the OCI registry | **artifact store** | Change and delivery |
| | a node's own copy of its last declaration | **last declaration** | Placement |
| record | a numbered decision | **decision record** | The record |
| | the literal reader of this repository, searched first for a symptom | **the record** (the `records` module) | Operator and conversation |
| | the upgrade policy that sends nowhere | `record` | Module |
| knowledge base | the module holding the decision records and research efforts whole | **knowledge base** | The record |
| | the predecessor's operational memory, in older playbooks (issue 430) | none: it no longer exists | — |
| check | a pull request's two statuses | **merge check**, **merge gate**, **repository check** | Change and delivery |
| | a delivery group's verdict | **composed check** | Change and delivery |
| | the controller's own examination | **self-check** | Health and repair |
| | a file in `00-META/checks/` | **hq check** | The record |
| agent | a coding agent | **agent** | Operator and conversation |
| | the build seat's holder | **builder** | Change and delivery |
| the user | an account of a wrapped program, or of the bus server | **its account**, **bus account** | Identity and access |
| | a human | **operator** or **person** | words every domain uses |
| firewall | what the mesh enforces | **packet filter** | Connectivity |
| | what was found on a machine | **found firewall** | Connectivity |
| ticket | an answer's one-time token (ADR 0258) | **answer ticket** | Operator and conversation |
| | a question someone must answer | **issue** | The record |
| deploy | the per-node part of a delivery plan | **deploy plan** | Change and delivery |
| | a wrapped program's own deploy | its own word | — |
## How this page is kept
A new name for an existing thing lands here first, in the same change that introduces it in code. A
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
term retired here may still appear there, and the mapping above is how to read it.
A new word for an existing thing lands here first, in the domain that owns it, in the same change that
introduces it in code. A word moves to another domain only with a decision record. A retired word goes
on a *Not:* line and nowhere else on this page, and `words.py` then fails on it in running prose. A
decision record keeps whatever word it was written with — those are immutable — so a word
retired here may still appear in one, and the *Not:* lines are how to read it.
**How it is checked** (ADR 0244):
`python3 00-META/checks/words.py`, run by `merge-check.sh` on every pull request, fails on a retired
word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and
issues opened from 2026-10-07 — code spans, quotations and link targets excepted — and on a
head word that heads two entries or is also retired. The words retired with no scope or with *(tools)*
are copied into the catalogue as `retired-words`, whose own repository check holds the descriptions of
the mesh's tools to them; a change here that retires such a word changes that copy in the same delivery.
A decision record or research effort proposed to the knowledge base is held to the same words by its
rules `retired-word` and `daemon-line`, which read this page (ADR 0290). Homonyms are checked by review.
+17 -17
View File
@@ -1,9 +1,9 @@
---
status: canonical
updated: 2026-08-23
derives: knowledge-base constitution page
updated: 2026-10-11
derives: the constitution page
decisions:
- 02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md
- "ADR 0020"
---
# How we build
@@ -37,13 +37,13 @@ incident behind it is not written down, and the fix is to write it down, not to
| Rule | What it means |
|---|---|
| **Never write to a production database directly** | No insert, update, delete or schema statement executed against production by hand. Schema changes go through numbered migrations; data changes go through application code or the module's own capabilities. Raw statements skip every side effect the proper path has — events, audit, cache invalidation, fan-out. |
| **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. [ADR 0013](../02-DECISIONS/0013-schema-changes-are-numbered-migrations.md) |
| **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. ADR 0013 |
| **Never bypass the pipeline** | No manual database edit, no manual restart as a workaround. Fix the cause and deploy. A workaround that works is a workaround that is never removed, and the next person cannot tell the node from its declaration. |
| **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** ([ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md), which supersedes [ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)). The links the installer still reconciles are a migration, not a permission. |
| **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** (ADR 0012, which supersedes ADR 0012). The links the installer still reconciles are a migration, not a permission. |
| **Never push directly to the main branch** | Branch, push, review, merge. Every merge is a human checkpoint, without exception — **including in this repository**. A documentation repository is not a lower tier of care; a decision record lands the same way a service does. |
| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. [ADR 0023](../02-DECISIONS/0023-approval-is-the-checkpoint.md) |
| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. ADR 0023 |
| **Never open a pull request unprompted** | A permissions list saying it is allowed is not a request. |
| **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. [ADR 0010](../02-DECISIONS/0010-delivery.md), and §5. |
| **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. ADR 0010, and §5. |
### A failed step must stop the steps after it — how it was earned
@@ -69,7 +69,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
- **Every runtime variable is declared.** A variable the module reads and the manifest does not
declare is invisible to the mesh: it will not be generated, injected, or audited.
- **Provisioned credentials arrive through declared requirements**, never hardcoded in code,
compose files or scripts. [ADR 0009](../02-DECISIONS/0009-modules-and-the-graph.md)
compose files or scripts. ADR 0009
- **Never install a package by hand.** A package is declared in the manifest and arrives the
way every other package does. A hand-installed package is invisible to the mesh: it is not
declared, not reproduced on the next node, and not present after a rebuild — and the node
@@ -82,7 +82,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
- **Every standalone application gets its own repository**, with a manifest at its root,
registered as a build source. Creating an application directory in the monorepo is a
convention violation and reviewers reject it.
[ADR 0015](../02-DECISIONS/0015-applications-live-in-their-own-repository.md)
ADR 0015
### Migrations
@@ -96,7 +96,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
surface is regenerated from the mesh database; a local edit survives one synchronisation and is
then silently overwritten, bringing back whatever it fixed. Use the mesh operation that owns
the value. If unsure whether a file is managed, ask the tooling — the answer is not visible
from the file. [ADR 0011](../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)
from the file. ADR 0011
---
@@ -121,7 +121,7 @@ for them. Do not merge them into one module: they are delivered to different nod
that must be assigned where half of it is unwanted is not a boundary either.
Coherence is a context. Delivery is a module. Relationships are edges, not folders.
[ADR 0009](../02-DECISIONS/0009-modules-and-the-graph.md)
ADR 0009
### Contexts integrate through the record, never through a shared schema
@@ -198,8 +198,8 @@ declaration was never wrong. Nothing ever asked the system.
### Search the record before forming a hypothesis
The first action on any error message, failing service or unexpected behaviour is to search the
operational memory for the literal error text — before a hypothesis, not after one fails. The
knowledge base is indexed on symptoms.
record for the literal error text (`records.records_search`) — before a hypothesis, not after one fails.
The search matches the words as written, so search with the error's own words.
This fires hardest on *familiar* ground, where a confident trail feels like progress. Two
entries were each rediscovered from scratch over several hours in a single session because the
@@ -214,7 +214,7 @@ This document is governed. It does not change by commit message or unilateral de
1. **Propose** — a change stating what rule is changing, why the current wording is
inadequate, and what reviewed it.
2. **Review** — sign-off by reviewers who are not the proposer.
3. **Record** — the change is a decision and gets a record in [`02-DECISIONS/`](../02-DECISIONS/), because a
3. **Record** — the change is a decision and gets a decision record in the knowledge base (playbook [02](process/02-graduation.md)), because a
rule the mesh enforces is architecturally significant.
4. **Sync** — playbook [`process/05-constitution-sync.md`](process/05-constitution-sync.md)
publishes the derived page. An unsynced rule is a rule the mesh does not enforce, whatever
@@ -226,7 +226,7 @@ No drive-by edits. Every change traces to a recorded decision.
a design meeting with at least two node operators — which has never been met and cannot be, as
there is one operator. A rule that cannot be satisfied is not a high standard; it is a rule
everything silently violates. Recorded here as resolved in favour of what is achievable, and
what has in fact been practised ([ADR 0022](../02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md)).
what has in fact been practised (ADR 0022).
---
@@ -243,7 +243,7 @@ process. Absence of an override means these rules apply unmodified.
## 8. Code quality
*Absorbed 2026-08-26 from the enforced page, which carried these rules while this document did
not — [ADR 0022](../02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md).*
not — ADR 0022.*
**These rules are recorded because they are enforced, not because this repository earned them.**
Every other rule here states the incident or measurement behind it. These state nothing,
@@ -272,7 +272,7 @@ Data access, business logic and the interface layer are separate.
*Scope: the mesh's services and surfaces. Tier 0 is a statically linked binary that must depend
on nothing installed first, and is written in Go —
[ADR 0005](../02-DECISIONS/0005-the-node-host.md).*
ADR 0005.*
- TypeScript throughout; no new untyped JavaScript.
- Strict, with no implicit `any` and no unchecked index access.
+1 -1
View File
@@ -11,7 +11,7 @@ updated: 2026-08-22
An agent states an intent — in words, from wherever they already are — and the mesh
carries it out. It takes the request in, works out what it means, does the work across
whichever nodes it needs, and returns a result. No console to open, no runbook to follow,
whichever nodes it needs, and returns a result. No terminal to open, no runbook to follow,
no remembering which node holds which thing.
Not automation, which does what it was told to do in advance. Self-control: the mesh
+26 -17
View File
@@ -3,8 +3,8 @@
How work moves through HQ, and who may do what. In industry terms this is **spec-driven
development, with provenance**: the decision is the why, the design doc is the spec, `code:`
names the implementation, and the lab beds are the conformance tests — and unlike the common
form, the chain itself is checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md),
[0081](../../02-DECISIONS/0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)). Every other document in this folder is a
form, the chain itself is checked (ADR 0080,
ADR 0081). Every other document in this folder is a
playbook: trigger, who runs it, steps, outputs. Engineers and agents follow the same
playbooks; agents must not act outside them.
@@ -19,11 +19,13 @@ playbooks; agents must not act outside them.
## The knowledge flow
```
idea ──► 01-RESEARCH ──► decision (02-DECISIONS/) ──► 03-DESIGN/01-to-be ──► built (code repo)
│ │ │
│ │ └─► 03-DESIGN/00-as-is once shipped
idea ──► research ──► decision ──► 03-DESIGN/01-to-be ──► built (code repo)
│ │ │
│ │ └─► 03-DESIGN/00-as-is once shipped
│ └────► abandoned (recorded, kept)
└─(small/obvious, still recorded in 02-DECISIONS)──────────► 03-DESIGN directly
└─(small/obvious, still a decision record)──────────► 03-DESIGN directly
research and decisions: in the knowledge base (knowledge-base.reserve, propose, ask, get, holds)
symptom ──► 04-ISSUES ──► diagnosis ──► code-repo fix and/or design amendment
@@ -37,7 +39,7 @@ how-we-build.md ──► constitution sync ──► knowledge base ──► i
| Layer | What it is | Changes when |
|---|---|---|
| `00-as-is/` | The mesh that exists today. Describes shipped behaviour, including behaviour nobody would choose again. | Something ships, or an as-is claim is found to be wrong. |
| `01-to-be/` | The mesh being built toward. Every statement traceable to a record in `02-DECISIONS/`. | A decision is taken or amended. |
| `01-to-be/` | The mesh being built toward. Every statement traceable to a decision record in the knowledge base. | A decision is taken or amended. |
A to-be document that ships does not move. Its as-is counterpart is written or updated, the
to-be document's frontmatter goes to `implemented`, and both stand — one describing what runs,
@@ -60,18 +62,25 @@ the expensive half.
The flow above is a rule, and a rule states how it is checked:
[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no
decision, an `in-progress`/`implemented` design that names no owning code, an issue marked
`located`/`fixed` with no owner or `fixed`/`resolved` with no fix, and a `graduated`
research overview that does not say what it became. Run it with `records.py` and `index.py`
before any HQ merge. What the checks cannot see — that code work actually started from a
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
sharing a number or an issue report carrying a status key; a `graduated` research effort that
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
refused at the write by the issue tracker's `move`
(ADR 0275). What the checks cannot see — that code work actually started from a
handoff — is held by playbooks [04](04-build-handoff.md) and [07](07-feature-branches.md):
a feature branch exists because a design or an issue sent it.
## Status lives in frontmatter
## Status lives in frontmatter, an issue's in the tracker
Research overviews, design docs, issue reports and decision records each carry their status as
YAML frontmatter (schemas in the section READMEs and playbooks). There are **no central status
Design docs carry their status as YAML frontmatter (schema in playbook [02](02-graduation.md)).
Research efforts and decision records carry theirs in the knowledge base, which refuses a wrong one at
`propose` (ADR 0290). An issue's status lives in the
issue tracker, which refuses a wrong status at the write, and its report here carries only
the day it was opened (playbook [03](03-issues.md)). There are **no central status
files** and no decision ledger. **Every decision is a record in
[`02-DECISIONS`](../../02-DECISIONS/)** — if it is worth recording it is worth a record, and if
it is not worth a record it is not recorded. Cross-cutting views, the decision index included,
are generated on demand by the `hq-status` skill and never written to disk.
the knowledge base** — if it is worth recording it is worth a record, and if
it is not worth a record it is not recorded. Cross-cutting views are generated on demand by the
`hq-status` skill and never written to disk (ADR 0248); the decisions of a topic are
`knowledge-base.holds` with that topic.
+37 -19
View File
@@ -5,25 +5,34 @@ design. Also: an as-is document that raises a question nobody can answer.
**Who runs it.** Anyone.
**Where it lives.** A research effort lives whole in the knowledge base, the seat `knowledge-base`, reached
through the mesh MCP server from any node and any folder (ADR 0290). This repository holds none: an effort
is written with the knowledge base's verbs, never as a folder here.
## Steps
1. Take the next free number: highest `01-RESEARCH/NNN-*` plus one, zero-padded to three
digits. Never skip or reuse a number.
2. Create `01-RESEARCH/NNN-descriptive-name/` and a `00-overview.md` in it carrying:
1. **Read what is known first.** `knowledge-base.ask` with the question in plain words returns the decision
records and research efforts that bear on it; `knowledge-base.get` with `kind: research` and a number
reads one effort, and `reasoning: true` (or `section` with a document's name) reads its documents.
2. **Take a number.** `knowledge-base.reserve` with `kind: research` and `for`: the effort's title. The
number comes from the issue tracker's counter and is spent whether or not the effort is ever proposed;
one never proposed is withdrawn after a week. Never pick a number by hand.
3. **Propose it.** `knowledge-base.propose` with `kind: research`, the `number` reserve gave, a one-line
`title`, `status: active`, `date` (the day it began), and `sections`: its documents in order, each a
`name` and its markdown `text`. The first is `00-overview.md`: **what** is being investigated, **why**,
and **what it touches**. Name what it touches as `links` of kind `touches` (`"0276"`, `"research 045"`,
`"issue 365"`, or a design's path), not only in prose, and what it names for another reason as `cites`.
4. **Do the research** in further documents of the same effort: notes, option analyses, evidence, draft
designs. Each change is `knowledge-base.propose` again with the effort whole and `why`, one line saying
why this version. Every version is kept; `knowledge-base.history` lists them and gives the difference
between two.
```yaml
---
status: active
initiated: YYYY-MM-DD
touches: [] # areas, subsystems or as-is design docs the effort bears on
---
```
Then a short prose summary: **what** is being investigated, **why**, and **what it
touches**.
3. Do the research in further documents in the same folder — notes, option analyses, evidence,
draft designs. Anything goes. Keep the summary in `00-overview.md` current as the effort changes
shape.
`propose` refuses an effort that breaks a rule that judges one record (an unknown status, a link to
nothing, a retired word of the glossary, a daemon's line), naming every reason at once and writing nothing.
The knowledge base then reviews the proposal itself, through a judgement that sees only the record, the
records it links and the rules; on `changes` the effort stays proposed with the findings, and its author
proposes it again. An effort that needs the operator's word is proposed with `needs-operator: true` and
waits for the operator's Approve on a channel that proves who answered.
## What makes research worth reading
@@ -37,12 +46,21 @@ carries the whole lesson without naming anything.
## Do not
- Do not put status in prose. It lives in `00-overview.md`'s frontmatter, and the prose must not restate it.
- Do not set `became:` while the effort is open — playbook 02 sets it at closure.
- Do not put status in prose. It is the effort's `status`, and the prose must not restate it.
- Do not give a `became` link while the effort is open — playbook 02 gives it at closure.
- Do not write into `03-DESIGN` from an open effort.
- Do not write an effort, or a copy of one, into this repository: the knowledge base is the only place one
is read, and `records.py` refuses a new path under `01-RESEARCH/`.
## Closing
An effort never just stops. It closes through playbook [02](02-graduation.md) as `graduated`
or `abandoned`, always with `became:` pointing at what it turned into. Nothing is deleted —
or `abandoned`, always with a `became` link to what it turned into. Nothing is deleted —
what was rejected, and why, is the more expensive half to rediscover.
## How it is checked
The rules on an effort are the knowledge base's: `propose` refuses one that breaks a rule that judges one
record (the module's table test holds one case per rule), and the rules that judge the whole record set run
after every acceptance and raise a condition when one fails. That this repository holds no effort is
`records.py`'s `no-records` check, on every pull request.
+89 -14
View File
@@ -4,15 +4,20 @@
**Who runs it.** Anyone, with the decision recorded before the design moves.
**Where a decision lives.** A decision record lives whole in the knowledge base, the seat `knowledge-base`,
reached through the mesh MCP server from any node and any folder (ADR 0290): its number, title, topic,
status, date, deciders, its clauses, its typed links, its reasoning and every version. This repository holds
none, and keeps no copy. What was decided is read with `knowledge-base.ask` (a question in plain words),
`knowledge-base.get` (one record by number; `reasoning: true` for its why) and `knowledge-base.holds` (the
clauses in force for a topic or a record).
## Graduating research
1. **Check it against GENESIS.** An effort graduates only if its conclusion is traceable to
[`mission.md`](../mission.md), [`context.md`](../context.md) and
[`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is —
say which, in writing, before proceeding.
2. **Record the decision.** Write a record in [`02-DECISIONS/`](../../02-DECISIONS/) taking the next free
number. Format and rules are in [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md). State evidence,
not assertion, and record the options that were rejected — that is the half worth keeping.
2. **Record the decision** (below, *Writing a decision record*).
3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter:
```yaml
@@ -21,25 +26,83 @@
status: designed
code: [] # owning code repo(s); set at build handoff, empty before
updated: YYYY-MM-DD
decisions: [02-DECISIONS/NNNN-....md]
decisions:
- "ADR NNNN"
---
```
4. **Close the effort.** Set the effort's `00-overview.md` frontmatter to `status: graduated` and
`became:` pointing at the design document and the decision record.
4. **Close the effort.** `knowledge-base.propose` the effort again, whole, with `status: graduated` and a
`became` link to the decision record (`"NNNN"`) and to the design document (its path). An effort that
stops without a decision is proposed with `status: abandoned` and a `became` link to what replaced it.
## Writing a decision record
1. **Take a number.** `knowledge-base.reserve` with `kind: decision` and `for`: the record's title. The
number comes from the issue tracker's counter and is spent whether or not the record is accepted; one
never proposed is withdrawn after a week. Never pick a number by hand.
2. **Propose it.** `knowledge-base.propose` with the `number`, a one-line `title` in plain language, its
`topic` (one of the six of the reading order, [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)),
`date` (when the decision was taken, not when it was written down), `deciders`, and:
- `clauses`: what was decided, in order, each one statement opening with a bold lead
(`"**What is decided.** …"`);
- `sections`: the reasoning, in order: `Context` (what was true, with evidence), `Considered Options`
(numbered, each with why it was rejected), `Consequences` (what follows, including what got harder),
`How it is checked` (a rule states how it is checked), `References`;
- `links`: `extends` for a record it builds on, `rests-on` for a decision it stands on, `supersedes` for one it replaces whole, `changes` with
the clause it replaces part of (`"0276 §4"`, and `from` for the clause of this record that does it),
`cites` for any record, effort, issue or design it names;
- `homes`: the documents of this repository that cite it, such as the design's path;
- `needs-operator: true` when it holds only once the operator approves it.
State evidence, not assertion. *"Zero of 124 modules declare `brain` as a dependency"* outranks *"the
dependency rule is not followed"*. Record the options that were rejected — that is the half worth keeping.
3. **A record that puts work off** — a rename, a check, a case it does not handle — names the issue that
tracks it, in the sentence that defers it ("tracked by issue N"), or the deferral is not accepted: a
deferral nothing tracks reads as done
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
ADR 0244's code renames). `propose` refuses it (rule `deferral`).
4. **Review and acceptance.** `propose` refuses, naming every reason at once and writing nothing, a record
that breaks one of the rules that judge one record: a link to nothing, a clause without a bold lead, a
superseded record cited alone, an unknown topic, a retired word of the glossary, a daemon's line, a
deferral no issue tracks. The knowledge base then reviews the proposal itself, through a judgement that
sees only the record, the records it links and the rules, and answers `approve` or `changes`. On
`changes` the record stays proposed with the findings: revise it by proposing it again, whole, with `why`.
On `approve` it is accepted, or, when it says `needs-operator`, it waits for the operator's Approve on a
channel that proves who answered. Nothing proposed is in force. `knowledge-base.get` shows a record's
reviews and the operator's ask when it waits for one.
## Changing a decision
**A record's meaning is never rewritten.** A decision is changed by a new record, and a correction of fact
by a progressive insight.
- **Reversing a decision, changing its scope, rejecting an option it accepted, or making a consequence false
that a later record cites** is a new record that `supersedes` the old one whole, or `changes` the clauses
it replaces. On its acceptance the old record or clauses become `superseded` or `changed`, and every
answer says by what. The test is not how large the edit looks; it is whether a reader who acted on the
old text would now be wrong about *what was decided* rather than about *a detail the decision did not
rest on*.
- **A progressive insight** corrects a fact a record asserted that turned out wrong while the decision it
supports stays right: a count taken before anyone measured, a file named that does not exist. It is
`knowledge-base.propose` of the accepted record with `insight`: what was found, what the record said
before, and what it says instead. It is a new version, marked as one, dated, and reviewed like a
proposal; it adds and never quietly replaces; the decision, the options weighed and the consequences
stand untouched. If the correction changes what was decided, it is not an insight — write the new record.
- **Where a later record changes the mechanism an older one states** without reversing it, the later
record `changes` the older one's clause, so a reader who arrives at the older record by a citation sees
what changed it since.
## Amending an existing design
A design changes only through a decision.
1. Write the decision record. If it reverses an earlier one, the earlier record's `status:`
becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its reasoning is never rewritten**. If the
earlier record is sound and only a *fact* in it went stale, that is a **progressive insight**,
corrected in place and marked in the record rather than superseded
([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)).
2. Edit the to-be design document and set `updated:` to today.
3. If the amendment came from an issue, set that issue's `amended-design:` to the document
path.
1. Write the decision record (above). If it reverses an earlier one, it `supersedes` it.
2. Edit the to-be design document, add the record to its `decisions:` as `"ADR NNNN"`, and set `updated:`
to today.
3. If the amendment came from an issue, give the issue the document path in the issue tracker:
`amended-design` on its `issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind
`design` before then — never in the report's frontmatter
(ADR 0275).
## When something ships
@@ -59,3 +122,15 @@ Implementation state is a third axis, independent of both design and decision.
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
the decision stands, is a progressive insight — marked and dated in the record, never silent.
- Do not write a decision record, or a copy of one, into this repository: `records.py` refuses a new path
under `02-DECISIONS/` other than its README.
## How it is checked
The rules on a record are the knowledge base's: `propose` refuses one that breaks a rule that judges one
record (its module's table test holds one case per rule), and the four rules that judge the whole record set
(an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing
changes, an accepted record extending a superseded one) run after every acceptance and raise a condition
when one fails. In this repository, `records.py` checks that a design's `decisions:` entries and every
"ADR NNNN" in the text are well formed, and that no record is written here; `index.py` checks the reading
order's six topics, which the knowledge base reads.
+86 -24
View File
@@ -8,53 +8,115 @@ design permits.
## What belongs here, and what does not
| Belongs in `04-ISSUES` | Belongs in the knowledge base |
| Belongs in `04-ISSUES` | Belongs with the fix's own lesson, not here |
|---|---|
| The design permits a failure to be silent | How to fix one occurrence of it |
| A documented rule is enforced by nothing | A command that works around it |
| A stated invariant is false in practice | A node-specific quirk |
| The owning component is unknown and finding it needs the whole mesh in view | Symptom → fix, once the answer is known |
The knowledge base already holds the operational record and is indexed on symptoms. This
folder is not a second copy of it. An issue here is a question HQ must **answer**, not an
A fix's lesson (symptom → fix) is written where a search by the symptom's literal words finds it
(`records.records_search`). This folder is not a second copy of it. An issue here is a question HQ must **answer**, not an
incident someone must **clear**.
## The issue tracker holds the number and the status
Every issue is opened in the **issue tracker**, the module `mesh-issues` holding the seat `issue-tracker` on the
control-node (ADR 0275,
[to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md)). Its number comes from one counter advanced by
compare-and-set, so two authors opening at the same moment never get the same one. **A number is never taken by
reading `main` or the open pull requests** — that is how numbers collided (issue 155: two records sharing a number
reached `main` with every check passing). A session that cannot reach the tracker takes no number and says so.
**The tracker is the truth for every issue's status, owner, fix and links**, whether or not the issue has a folder
here. `issue-tracker.get NNN` says where an issue stands; `issue-tracker.list` says what is open. Nothing here
says it: a report's frontmatter carries no status.
An issue takes one of two shapes:
- **With a folder here** — a design or governance issue, whose narrative belongs in this public record and is
reviewed in a pull request. The folder holds the report and the diagnosis; the tracker holds everything else.
- **Wholly in the tracker** — every other issue. Nothing is written here.
**`04-ISSUES/` stays, for the narrative only.** It keeps the public narrative of a design or governance issue: the
symptom as observed, the diagnosis, what was ruled out, and why. A plain bug — a defect whose fix needs no design
question answered — needs no folder; it is whole in the tracker.
**The reports written before the tracker** (the folders numbered 001 to 364) carried `status:`, `located-in:`,
`fixed-by:`, `amended-design:`, `replay:` and `replay-none:` in their frontmatter until the import of slice 3 had
taken them into the tracker and been checked against it, field by field; then one pull request removed them, so
that no status is held in two places. What they said that day is in this repository's history at the commit the
import read, which every imported issue names (`imported.commit` in `issue-tracker.get`).
## Steps
1. Take the next free number — **across `main` and every open pull request**, not `main` alone.
Work sits on unmerged branches for days, so two people both reading `main` allocate the same
number; it happened twice in one hour between two machines, and the second collision reached
`main` with every check passing (issue 155). `cycle.py` now refuses two records sharing a number,
which catches a collision but does not prevent one. Create
`04-ISSUES/NNN-short-name/00-report.md`:
1. Open the issue in the tracker, through the mesh MCP server:
`mesh_call issue-tracker.open` with `title`, `report` (the symptom **as observed**, in plain terms, with the
evidence that it happened), optionally `located-in` (entries of the tracker's registered list of owners), and, for
an issue that gets a folder here, `record` — the folder's name without its number, `short-name`. The tracker
answers with the number `NNN`; a refused open spends none.
**An issue with a folder** then creates `04-ISSUES/NNN-short-name/00-report.md` under exactly that number, on a
branch:
```yaml
---
status: open
opened: YYYY-MM-DD
located-in: [] # owning repo(s)/module(s), filled by diagnosis
fixed-by: # PR or commit reference, filled at resolution
amended-design: # design doc path, when the root cause was a design gap
---
```
Then the symptom **as observed**, in plain terms, with the evidence that it happened.
2. Investigate in `01-diagnosis.md` in the same folder — the trail, dated, including what was
ruled out. Move `status:` to `diagnosing`, then `located` once the owner is known.
3. Resolve. Set `status: resolved`, fill `fixed-by:`, and if the root cause was a design gap,
run playbook [02](02-graduation.md) and fill `amended-design:`.
Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
`replay:` or `replay-none:` in any report, because the tracker holds them and two places holding one fact
drift.
**An issue about a condition** links the condition's key at once: `issue-tracker.link` with kind `condition` and
the key `mesh-controller.conditions` gives. Every message about that condition then names the issue and where it
stands; one no issue links says "No issue tracks this yet." An agent that silences a condition because an issue
tracks it links that issue first (ADR 0285).
**An issue wholly in the tracker** is done at this step.
2. Investigate. **Before you start, move the issue to `diagnosing` with `by`**: who works on it and its node, as
who@node (`fix-399@laptop`, `operator@workstation`). The tracker keeps you as the issue's **worker**, and `get` and `list`
show it with since when. The trail goes in `issue-tracker.note`, and, for a folder-backed issue, in
`01-diagnosis.md` in the same folder as well — dated, including what was ruled out. A note keeps you the worker; a
worker with nothing done on the issue for a day is stale. Then move it to `located` with `located-in`, which the
tracker refuses without a registered owner; that ends your work on it.
3. Fix. **Before you start the fix, move the issue to `fixing` with `by`**, even when you diagnosed it yourself. The
tracker refuses the move while another worker is current, naming them: ask them, or take it over with
`take-over` saying why, which is noted on the issue. A stale worker may be taken over from without a reason.
Name the fix in `fixed-by` when it merges, with a move to `fixing` that gives `by` again; the issue stays
`fixing` until the acceptance is proven live
(ADR 0284,
ADR 0281).
4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap,
run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in`
names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build
agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on
the commit before the fix and pass on it, or with `replay-none` saying why none is possible
(ADR 0237);
`issue-tracker.move` refuses the move without it. `wontfix` needs a `reason`.
## Rules
- Closed issues are never deleted — they are the mesh's symptom-to-component memory.
- `fixed-by:` names something that will still exist: a commit or a pull request, never a branch. A
- `fixed-by` names something that will still exist: a commit or a pull request, never a branch. A
branch is deleted when it merges, so a branch name there is a pointer that resolves to nothing by
the time anybody follows it.
- A fix that turns out to have broken something else is written back into the record that asked for
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
is must not have to already know there was a sequel.
- Renumbering a collision happens once, in the branch that lands last. Renumbering a branch whose
author is still pushing only moves the race.
- An issue whose answer is a general lesson should also be written to the knowledge base, so
the next person searching a symptom finds it. Both, not either.
- `status: wontfix` is legitimate and requires a sentence saying why.
- A number never moves. A folder carries the number the tracker gave; a folder whose number collides with
another (one numbered before the tracker, or by hand) is a fault, and `cycle.py` refuses two records
sharing a number.
- An issue whose answer is a general lesson should also have that lesson written where a search by
the symptom's literal words finds it, so the next person searching it finds it. Both, not either.
- `wontfix` is legitimate and requires a sentence saying why; the tracker refuses it without one.
- **An agent says when it starts work on an issue**, by moving it to `diagnosing` or `fixing` with `by`. Two agents
never start the same work without knowing: the second is refused, naming the first.
- **A rule states how it is checked.** The cycle's rules on an issue — a status one of the six, `diagnosing` and
`fixing` with a worker and no other current one, `located` with an owner, `resolved` only from `fixing` and with a
fix, a core issue with its replay, `wontfix` with a reason — are refused at the write by
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). `cycle.py` checks what
this repository still holds: no two folders share a number, every report has frontmatter with the day it was
`opened:`, and no report carries a status key.
+6 -1
View File
@@ -7,7 +7,8 @@
## Steps
1. **Confirm the design is settled.** Its frontmatter reads `status: designed`, and every
claim in it traces to a record in [`02-DECISIONS/`](../../02-DECISIONS/). An open question in the text is a
claim in it traces to a decision record in the knowledge base (`knowledge-base.get` for each entry of
its `decisions:`). An open question in the text is a
reason to run playbook [01](01-research.md), not to start building around it.
2. **Name the owner.** Set `code:` in the design's frontmatter to the repositories from
[`repos.md`](../repos.md). If the repository does not exist yet, add it to `repos.md` in
@@ -24,6 +25,10 @@
## Rules
- Every merge is a human checkpoint, without exception.
- What the handoff leaves for later is an issue with an owner, named in the design or the decision that
leaves it — never a sentence saying "later". The design's `code:` names who owns what is built; the issue
names who owns what is not. the knowledge base's rule `deferral` checks the decision record when it is proposed
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)).
- Never open a pull request unprompted. A permissions list saying it is allowed is not a
request.
- Work in an isolated worktree, never a shared checkout. A failed `cd` in a shared checkout
+3 -3
View File
@@ -1,7 +1,7 @@
# Playbook 05 — Constitution sync
Implements [ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md): HQ is
the source of the constitution, and the knowledge-base page is derived, never edited.
Implements ADR 0021: HQ is
the source of the constitution, and the constitution page is derived, never edited.
**Trigger.** [`how-we-build.md`](../how-we-build.md) changed a rule that the mesh enforces at
runtime.
@@ -24,7 +24,7 @@ and, per the repository's own rule, it is how the rule "HQ is the source" is che
the derived page carries the rule alone.
2. Record the change as a decision — a rule the mesh enforces is architecturally significant.
Playbook [02](02-graduation.md).
3. Publish the derived page to the knowledge base under the constitution slug, replacing its
3. Publish the derived page where the mesh keeps the constitution page, under its slug, replacing its
body. Keep the section numbering stable: the meeting orchestrator and the review fragments
cite sections by number.
4. Verify the derived page reads back with the change present. A publish that reported success
+5 -5
View File
@@ -25,7 +25,7 @@ Three questions, answered from the machine:
## The steps
1. **Name what it provides and requires**, if anything. A name is what a consumer is coupled to,
not the role it plays ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
not the role it plays (ADR 0027):
`postgres-database`, not `database`. Most modules provide nothing and require nothing — an
application is usually a leaf.
@@ -52,7 +52,7 @@ Three questions, answered from the machine:
the container env-file: [/var/lib/postgres/superuser.env]
```
The host fills the hole on the machine, which is the only place both halves exist — the mesh
The node-engine fills the hole on the machine, which is the only place both halves exist — the mesh
discarded the value
([credentials and their rotation](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)).
A **provisioner** is the exception: it reads a password file, so it mounts the `.secret`
@@ -89,7 +89,7 @@ Six attempts, one real bug. Recorded because the ratio is the lesson: **the mesh
time and the scaffolding was not.**
- A shape existed in the language and no host implemented it, so every declaration carrying one
was refused whole — correctly, and the host said exactly that. **Nobody was reading the host's
was refused whole — correctly, and the node-engine said exactly that. **Nobody was reading the node-engine's
log.** Read it first; it is the only place that says why a machine did nothing.
- A blind find-and-replace renamed a provision in quotes and missed the same word bare.
- A command was tested only for the invocations that should fail, so it rejected every real one
@@ -99,9 +99,9 @@ time and the scaffolding was not.**
## Rules
- **Read the host's log before theorising.** A declaration that was sent and not applied says so
- **Read the node-engine's log before theorising.** A declaration that was sent and not applied says so
there and nowhere else.
- **A failing test is kept, not skipped.** It is the reproduction.
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
- **A data directory is never removed by the mesh** ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)),
- **A data directory is never removed by the mesh** (ADR 0030),
and that protects against the mesh only — not against a disk or a mistaken command.
+1 -1
View File
@@ -43,7 +43,7 @@ One feature is **one branch name**, **one worktree per repo**, **one MR per repo
4. **Finish, then publish.** When the whole feature is done — every repo, tests green — push
every branch and open **one MR per touched repo**, together.
5. **Merge promptly, once approved.** Every merge into `main` is notified and approved
([ADR 0023](../../02-DECISIONS/0023-approval-is-the-checkpoint.md)); once it is, merge —
(ADR 0023); once it is, merge —
do not leave it sitting. The branch is deleted on merge.
6. **Leave nothing behind.** After the MRs merge, no `feat/<slug>` branch and no `.work/<slug>`
worktree survive.
+9 -9
View File
@@ -15,30 +15,30 @@ and a forge address is an operational detail (see [`README`](../README.md)).
| Repository | Owns |
|---|---|
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped ([ADR 0019](../02-DECISIONS/0019-how-this-repository-works.md)); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped (ADR 0019); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
| *(one per application)* | Every standalone application, site or side-project gets its own repository, with `module.yml` at the root. Registered with the mesh as a build source; built and deployed by the same pipeline as anything in the monorepo. |
| `migration` | **Private.** The record of one installation replacing the predecessor mesh with this one: the runbook, a dated log of every step and what it cost, the per-service data procedures, the readiness checks, and the scripts. Private because it is the opposite of this repository in every way that matters — it names machines, addresses, ports and paths, because a procedure that cannot be followed is not one. Where hq asks *what did we decide and why*, that repository answers *what happened on the machines, in what order, and what to do next*. Its `HANDOFF.md` is where somebody picking the work up starts. |
## What the mesh becomes
[ADR 0019](../02-DECISIONS/0019-how-this-repository-works.md) records the repositories the
ADR 0019 records the repositories the
monorepo decomposes into. **`mesh-host`, `mesh-controller`, `mesh-catalog`, `mesh-lab`, `mesh-sdk` and `mesh-tools` exist
so far** — the lab was built first ([ADR 0016](../02-DECISIONS/0016-the-lab.md)). The tiered
so far** — the lab was built first (ADR 0016). The tiered
decomposition below is the planned shape; the repositories built to date do not map onto it
one-for-one — `mesh-catalog`, `mesh-sdk` and `mesh-tools` exist where the table names
`mesh-foundation` and `mesh-surfaces`, and reconciling the two is itself still ahead.
| Repository | Tier | Holds |
|---|---|---|
| `mesh-host` | 0 | **exists.** The node host — one statically linked binary, requiring nothing present ([ADR 0005](../02-DECISIONS/0005-the-node-host.md)) |
| `mesh-host` | 0 | **exists.** The node-engine — one statically linked binary, requiring nothing present (ADR 0005) |
| `mesh-foundation` | 1 | the four pinned services, as declarations |
| `mesh-controller` | 2 | **exists.** The controller and its contexts — one of seven built ([ADR 0006](../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
| `mesh-controller` | 2 | **exists.** The controller and its contexts — one of seven built (ADR 0006) |
| `mesh-surfaces` | 3 | tools, web, cli |
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile ([ADR 0039](../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)). |
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile (ADR 0039). |
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
Tier 4's shape is open, and deliberately so: see ADR 0019 and
[research 005](../01-RESEARCH/005-domain-grouping/00-overview.md).
research 005.
## What lives where inside the monorepo
@@ -56,7 +56,7 @@ Named by role, because the layout is itself part of the as-is design — see
## Why applications do not live in the monorepo
A standalone application in the monorepo is a convention violation, and reviewers reject it.
The reasoning is recorded in [`02-DECISIONS/0010`](../02-DECISIONS/0015-applications-live-in-their-own-repository.md):
The reasoning is recorded in ADR 0015:
the mesh installs, provisions for, and ships an application through exactly the same machinery
whether or not its source sits beside the mesh's own — so co-location buys nothing and costs
the monorepo's review cadence.
@@ -67,7 +67,7 @@ Each module is a standalone package that consumes its dependencies from the priv
not from a sibling directory. The workspace was removed after it caused build-versus-development
divergence — a workspace member importing another resolved to local unbuilt source in the
pipeline and to a published version in development. Recorded in
[`02-DECISIONS/0007`](../02-DECISIONS/0014-no-npm-workspace.md).
ADR 0014.
Consequence, and it is a real one: a cross-package change is two steps — publish, then consume
— and a repository-wide `npm install` does not exist.
@@ -1,67 +0,0 @@
---
status: active
initiated: 2026-08-22
touches: [03-DESIGN/00-as-is/02-modules-and-manifests.md, 03-DESIGN/00-as-is/10-module-catalogue.md, 03-DESIGN/01-to-be/00-work-breakdown.md]
became: [02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md]
---
# 001 — Module domain decomposition
- **Initiated by:** jochen, 2026-08-22
- **Areas touched:** every `hal/*` and `noxflow/*` module; the pipeline's dependency
graph; the knowledge base; agent identity and credentials.
## Summary
HAL has 124 modules. That number is not a maintenance problem in itself — it is the
**symptom of missing bounded contexts**. Modules are split not because they model
different domains, but because splitting is the only lever the platform offers:
- no way to run one daemon on one node without making it a module
(`hal/claude-licences` — one daemon, single-node)
- no way to expose two of a kind from one module
- no namespace separating the mesh from the software it runs
This effort establishes the **current state**, the **ideal state**, and the sequence
between them.
## Trigger
A night of debugging that produced four fixes and one conclusion. Every fault was a
boundary fault:
- Per-agent Claude credentials had to be written by the *noxflow runtime*, because
`agents.claude_account` is in noxflow's database — even though agent identity is a
mesh concept and node identity already lives in the mesh registry.
- Whether `hal/brain` may depend on noxflow took three attempts to answer, twice
wrongly, because the ownership boundary was never stated.
- Authoritative documentation existed in `mesh_docs` and was not found, while a
proposal in repo markdown was invisible to search entirely.
## Decisions taken (2026-08-22)
| Question | Decision |
|---|---|
| What should noxflow become? | Decompose into `hal/*` modules; noxflow returns to tasks/workflows |
| Who owns agent identity? | `hal/agents` — a mesh concept, alongside nodes |
| Where do third-party apps live? | Out of this repo. They run *on* the mesh; they are not *of* it |
| Knowledge structure | Modelled on `papa-hq`; implementation choice left open |
## Open questions
Tracked in [`analysis.md`](analysis.md) under "Open questions".
## Deliberately not decided
Recorded so they are not mistaken for oversights. Each is open, and each comes out of
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md); this effort stays
`active` until they are answered.
| Question | Status |
|---|---|
| `hal/scheduler` — infrastructure, or part of the work context. | Open. |
| Which context owns the executor. | Open. |
| Catalogue destination — one repository or many. | Open. Phase 4. |
| What the shared library keeps after extraction. | Open. Phase 3. |
| Where human agent modality is recorded — which user, on which node, a human agent acts as. | Open. Required by the model; not yet stored. |
| Which domains the modules outside the platform core group into. | Open, from [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), which settles the principle and deliberately not the list. |
@@ -1,242 +0,0 @@
---
effort: 001-module-domain-decomposition
updated: 2026-08-22
---
# Current state → ideal state
## 1. The count is a symptom
124 modules. The number is not the problem; the reason for it is. Modules are split
because splitting is the **only granularity lever the platform offers**:
| symptom | module | why it exists |
|---|---|---|
| one daemon must run on one node | `hal/claude-licences` | no per-node feature opt-in |
| a library must not drag a 450 MB dep | `hal/claude` | no way to expose two npm packages |
| a domain needs a daemon *and* a library | `hal/claude-code` + `hal/claude` | one feature of each type per module |
| a handful of SDK verbs need a home | `infra` (10 lines) | tools must belong to *some* module |
A feature is a **handler type**, discovered by directory presence, singleton per module
(`FEATURE_HANDLERS` in `modules/hal/sdk/src/feature-handlers/index.ts`). So "one more
deployable unit" always means "one more module".
Splitting also fragments domains. Claude licence logic sits in three modules —
resolution in `hal/claude`, health in `hal/claude-code`, refresh in
`hal/claude-licences` — so following one token means reading three.
## 2. Two things are conflated in one repo
`noxflow` is 45 tables. Its original purpose — a HAL-native Jira — is 7 of them:
| domain | tables |
|---|---:|
| tasks (the original concept) | 7 |
| agents / HR | 13 |
| knowledge (a Confluence) | 8 |
| meetings | 7 |
| scheduling / ops | 2 |
| other | 8 |
Agents alone are nearly double the concept the module was built for. This is the direct
cause of concrete faults: per-agent Claude credentials had to be written by the noxflow
runtime purely because `agents.claude_account` lives in noxflow's database, while node
identity — the same kind of fact — lives in the mesh registry.
Separately, 91 of the 124 modules are third-party software (`plex`, `sonarr`,
`firefox`, `postgres`). These **run on the mesh; they are not part of it**. They belong
outside this repository. The pipeline and provisioning system are deliberately generic,
so HAL's own modules dogfood exactly what external modules use — that property is what
makes the separation safe.
## 3. Knowledge is split three ways
| store | owner | size | search |
|---|---|---:|---|
| `mesh_docs` (+ revisions) | `hal/hippocampus`, in the mesh DB | 144 docs, 73 touched in 30d | yes |
| `knowledge_*` (8 tables) | `noxflow/runtime`, in the noxflow DB | 20 pages | yes |
| repo markdown | git | `VISION.md`, `FLAVOR-PROPOSAL.md`, `docs/` | **no** |
The librarian is split across two of them: `librarian_file_document` writes via
hippocampus, `librarian_ask` and `librarian_retrieve` read via noxflow. Filing and
retrieval, different modules, different databases.
Both DB stores have revisions; only noxflow has a review workflow (`_proposals`,
`_promotion_requests`). Neither has file access. Git has file access and history but no
search integration. **Every piece needed already exists — none of them together.**
## 4. `hal/developer` and the missing local mesh
`hal/developer` (≈2,600 lines) exposes `dev_stage`, `dev_diff`, `dev_validate`,
`dev_typecheck`, `dev_verify_flavor`, `dev_bootstrap`, `dev_build`, `dev_up`, `dev_down`,
`dev_env`. It is the tooling that both reflects and enforces the module model, so it
moves with any change to that model.
`dev_up` builds a dev environment **for a single module**: resolves `requires:`, starts
providers "via systemctl (same as production)", provisions workspace-namespaced
databases. It borrows the host node's services.
**There is no way to run a mesh locally.** Consequently anything that only manifests
*between* nodes — credential rotation reaching a running session, provision credentials
fanning out, cascade ordering, artifact packaging — is discoverable only in production.
Every fault fixed on 2026-08-22 was of that kind.
A containerised mesh (N nodes, a broker, a registry, a coordinator) is therefore not a
convenience. It is the precondition for a refactor of this size being verifiable at all.
## 5. `hal/sdk` is where domains go to hide
155 files, 34,636 lines. It contains code from essentially every bounded context:
| file | domain it belongs to |
|---|---|
| `installer-core.ts` (1,495) | runtime |
| `tools/provisions.ts` (1,311) | provisioning |
| `tools/deploy-service.ts` (1,081) | deploy |
| `workflow-engine.ts` (970), `task-commands.ts` (588) | **work** — noxflow's domain, inside HAL's SDK |
| `artifact-manager.ts` (948), `build-executor.ts` (760), `feature-handlers/*` | delivery |
| `env-generator.ts` (944) | config |
| `amqp-client.ts` (899) | comms |
| `meshware.ts` (799) | runtime |
| `module-registry.ts` (743) | mesh |
| `claude-credentials.ts` | **ai** |
Nothing here was misplaced carelessly. Each landed in the SDK because the SDK is the one
package every module may import — so putting a shared type there is the way to avoid a
circular dependency between two modules that both need it.
The result is that the dependency graph is trivially acyclic and completely
uninformative: everything depends on one 34k-line package, so a change anywhere in it
rebuilds everything. On 2026-08-22 a three-line fix in `feature-handlers/migrations.ts`
produced a 1,445-job, 26-minute mesh-wide cascade.
The last row is from that same day, and is the clearest illustration: per-agent Claude
config-directory helpers were added to `@hal/sdk/claude-credentials.ts` because it was
the only place both `hal/claude` and the noxflow runtime could import from. The
alternative — `hal/ai` owning it and noxflow depending on `hal/ai` — was unavailable
without answering the ownership question this effort exists to answer.
**Extraction is therefore blocked on the decomposition, not the reverse.** A domain
module can only own its shared code once the domain has a module. The SDK should retain
only what is genuinely cross-cutting: transport, manifest types, logging, process
helpers.
## 6. The crux: two notions of "agent"
The original vision was **a node is an agent with thinking abilities**. Later, noxflow
implemented actual agents — employees with skills, workspaces and tasks. Both survive,
and they collide.
The collision is visible in the data. There are **two agent rows per node**:
| agent | display name | skills | bound to |
|---|---|---|---|
| one named after each node | the node's name | `{deploy,verify,operate}` | its node |
| `hal-<node>`, one per node | "HAL (\<node\>)" | `{}` | its node |
One carries the **work**; the other carries only **identity** — no skills at all, existing
to hold a licence for HAL's own sessions (`resolveHalModuleEnv`). The node-as-agent idea
was implemented twice, half each.
The same split appears in the schema: `nodes.hal_claude_account` and
`agents.claude_account` are the same fact in two tables in two databases. What was
documented as "three licence touchpoints" (NODE, HAL, AGENT) is **one concept modelled
three times**.
### Resolution
**Nodes and agents are decoupled.** The agents module owns agents; agents run on nodes.
A node is a place where an agent can run — that is the whole of the relationship. There
is no resident agent, no node-owned identity, no ownership in either direction.
Agents named after a node still exist, but they are **ordinary agents defined through the
agents module** that happen to hold infra skills and be named after the node they usually run
on. Nothing about them is structural.
Two consequences follow:
1. **`nodes.node_license` and `nodes.hal_claude_account` should not exist.** A node does
not authenticate to a model provider — agents do. Both collapse into
`agents.claude_account`, and what was documented as three licence touchpoints becomes
one.
2. **The pair of agent rows per node merges.** `hal-<node>` exists only to hold a licence
for HAL's own sessions; once licences belong to agents and any module may employ an
agent, it has no reason to be separate from `<node>`.
What remains unaccounted for is the operator's own interactive session:
`~/.claude/.credentials.json` on a node serves a **human at a prompt**, not an agent.
Under this decoupling that is not a node property either — it is the operator's
credential on whichever machine they are sitting at. Whether the operator is modelled as
a persona (soulstream treats humans and agents as peers with identical credentials) is an
open question, and the last remaining reason `nodes` carries a licence column today.
## 7. The record, not "meetings"
soulstream models work as **one signed log** in which humans and agents apply identical
changes, with work arriving by **mention**. Memory and decisions live in that single
auditable stream — no hidden state between components. Its stated economics,
*"cheap awareness and expensive thinking"*, is the same principle as the budget guard:
constant monitoring, selective deep processing.
Under that model "meetings" is not a domain. A meeting is a **thread in the record**.
Which means six of today's modules and table groups are one thing seen from different
angles:
`hal/axon` (DMs, rooms) · `hal/cortex` · `hal/synapse` · `hal/notifications` ·
`conversations` · `meetings`
A notification, in particular, is just a mention not yet read — which is why it needed
its own table, its own id column, and its own failure mode.
## 8. Ideal state
The mesh repository contains only the mesh, decomposed by domain rather than by
deployment accident:
| context | what it is | absorbs today's |
|---|---|---|
| `hal/infra` | **the room** — nodes, registry, provisioning, pipeline, node runtime | `hal/mesh`, `hal/brain`, `hal/coordinator`, `hal/meshware`, `hal/developer`, `hal/bootstrap` |
| `hal/agents` | **the name and the thinking** — Agent owns identity, licence, runs, memory, thoughts | noxflow agents (13 tables), `hal/thoughts` |
| `hal/stream` | **the record** — mentions, threads, meetings, notifications | `hal/axon`, `hal/cortex`, `hal/synapse`, `hal/notifications`, meetings (7), conversations |
| `hal/knowledge` | documents, spaces, revisions, review, search | `hal/hippocampus` + noxflow `knowledge_*` |
| `hal/work` | tasks, workflows, boards — the original noxflow | noxflow tasks (7 tables) |
| `hal/ai` | provider integration, flavored per vendor | `hal/claude*`, `claude-code` |
| `hal/config` | env and config distribution, secrets, PKI | `hal/env-sync`, `hal/config-sync`, `hal/secrets`, `mesh-ca` |
| `hal/observability` | health, dashboards, logs | `hal/health`, `hal/meshboard` |
Eight contexts instead of 33 platform modules, with the catalogue elsewhere.
The naming corrects itself in the process: what is called "brain" today is
infrastructure, and the word survives only as a concept an Agent owns — never again as a
module name.
This shape depends on **named features with per-node opt-in** — without it, every
independently-deployable unit inside a context becomes a module again and the count
returns. That mechanism is the subject of a separate ADR.
## Decided since
| question | decision |
|---|---|
| Meetings — own context or part of agents? | **Neither.** A meeting is a thread in the record; `hal/stream` absorbs it. The category was wrong. |
| Thoughts — autonomy or agent behaviour? | `hal/agents`. Any agent can have a thought. |
| `hal/axon` vs `hal/cortex` | `hal/cortex` is vestigial — no `daemon/` source, unit inactive on every node, description copy-pasted from axon. Messaging belongs to `hal/stream`. |
| `hal/hypothalamus` | Orphaned build output. A `dist/` directory with no `module.yml`, so not a module at all. |
| Who owns agent identity? | `hal/agents`. Nodes and agents are decoupled; a node is only a place an agent can run. |
| Where do third-party modules live? | Outside this repository. They run *on* the mesh, not *of* it. |
## Open questions
1. **`hal/scheduler`** — infrastructure (fire an event later, belongs to `hal/mesh`) or
part of `hal/work`?
2. **The executor.** Pulling and routing work items is `hal/work`; spawning a session is
`hal/agents`; providing the place to run is `hal/mesh`. It currently straddles all
three, which is the same kind of straddle that put credential-writing in the noxflow
runtime.
3. **Catalogue destination** — one repository, or per-application repositories, given the
pipeline resolves dependencies across the registry rather than the filesystem?
4. **SDK residue** — after extraction, does `hal/sdk` keep transport (`amqp-client`), or
does that belong to `hal/stream`? Everything imports it, which argues both ways.
5. **Human agent modality.** ADR 0001 requires a fact the mesh does not record: which
user, on which node, a human agent acts as. Where does it live — an attribute of the
agent, or of the agent-node binding?
-59
View File
@@ -1,59 +0,0 @@
---
status: graduated
initiated: 2026-08-22
touches: [03-DESIGN/00-as-is/04-delivery.md, 03-DESIGN/00-as-is/05-runtime-and-installation.md]
became: [03-DESIGN/01-to-be/01-end-to-end-testing.md, 02-DECISIONS/0016-the-lab.md]
---
# 002 — A mesh that runs locally
- **Initiated by:** jochen, 2026-08-22
- **Areas touched:** `install.d/`, `hal/meshware`, `hal/coordinator`, `hal/brain`,
`hal/developer` (`dev_up`), `hal/sdk` (env generation, feature handlers, artifact
manager), `test/pipeline/`, `test/dev-mesh/`, the provisioning path in
`modules/postgres/`.
## Summary
Phase 0 of [`03-DESIGN/00-work-breakdown.md`](../../03-DESIGN/01-to-be/00-work-breakdown.md) requires
a mesh that comes up in containers, runs its own pipeline, and reproduces known faults on
demand. Nothing else in the decomposition starts until it exists, because every fault the
decomposition addresses was found in production — there was nowhere else to find it.
This effort establishes what already runs in a container, what is welded to the host, and
what it would take to close the gap. It does **not** choose an approach: the central
question — how a containerised node executes a module service, when a module service is
defined today as a systemd unit shelling to `docker compose` in `/services/` — is not
answered by ADR 0001 and is recorded below rather than decided.
## What was established
- The two existing container harnesses are **neither of them a mesh**, and one of them has
not been able to build since 2026-06-04.
- Node identity is already portable — a single environment variable, no host handshake.
- Four concrete host couplings block a containerised node, all with known locations.
- All three Phase 0 fixtures are reproducible; one of them is documented in the knowledge
base with an open root cause and is the cheapest place to start.
Detail and evidence in [`analysis.md`](analysis.md).
## Questions — all settled 2026-08-22
**Phase 0 is a development environment**, not a fixture rig — it is what the host-borrowing
dev tooling becomes.
**The trigger is a real source-forge container**, because the webhook relay is part of what
is under test.
**A node is a system container**, promotable to a virtual machine per node. This answered
the question the effort was stuck on, and dissolved it rather than solving it: against a
real node with a real init, the four host couplings catalogued in `analysis.md` §3 are not
couplings — they are how a node works. They were obstacles only to a node modelled as an
application container.
Consequently [`003-service-supervision`](../003-service-supervision/) **no longer blocks
Phase 0**. It remains a live architecture question, on its own timeline.
The remaining questions in `analysis.md` — what replaces host paths in a container, and how
faithful the lab must be — are answered in the design: nothing replaces them, because the
paths are real; and the divergences are enumerated rather than discovered.
-263
View File
@@ -1,263 +0,0 @@
---
effort: 002-local-mesh
updated: 2026-08-22
---
# A mesh that runs locally — current state and obstacles
Evidence for Phase 0. Every claim here is either a file location or something measured on
2026-08-22; where a claim was checked and found false, that is recorded too.
---
## 1. What exists today, and why neither is a mesh
### `test/pipeline/` — the only containerised HAL node, and it is dead
This harness builds `@hal/sdk` and the `hal/brain` daemon into a `node:23-alpine` image,
compiles `hal/coordinator`'s tools against it, and runs `brainstem.js` with
`HAL_MODE=daemon` against a containerised postgres and LavinMQ
(`test/pipeline/Dockerfile`, `test/pipeline/docker-compose.yml`). It proves the valuable
thing: **a node runtime needs no systemd, no `/services/`, and no nvm** — environment
variables and a seeded `nodes` row are sufficient.
It also cannot build. Measured 2026-08-22:
```
Step 7/16 : RUN npm run build -w modules/hal/sdk
npm error No workspaces found:
npm error --workspace=modules/hal/sdk
```
The npm workspace it depends on was removed on 2026-06-04 (`21ef4a4e`, "kill npm
workspace"), and the root `package.json` now carries a comment explaining why it will not
come back. The harness has not been touched since before that commit.
So the repository's only end-to-end pipeline test has been unrunnable for two and a half
months and nothing reported it — which is the same shape as the faults Phase 0 exists to
catch. **A test nobody runs is indistinguishable from a test that passes.**
### `test/dev-mesh/` — a work plane, not a control plane
Four `runtime-*` services, a noxflow API, a meshboard and a shared postgres + LavinMQ on
one network (`test/dev-mesh/docker-compose.yml`). It is genuinely useful and the topology
is a good precedent: one network, per-service `HAL_NODE`, `EXECUTOR_STUB=1` so dispatch and
run lifecycle are real while the model call is faked.
It is the wrong layer for Phase 0, on three counts:
| | dev-mesh does | Phase 0 needs |
|---|---|---|
| state | restores `pg_dump`s (`test/dev-mesh/init.sh`) | schema built by **migrations**, or fixture 3 is untestable by construction |
| code | bind-mounts pre-built `dist/` from the host | artifacts **built and delivered** by the pipeline |
| scope | noxflow runtime, API, meshboard | `hal/coordinator`, `hal/meshware`, MinIO, provisioning |
No meshware, no coordinator, no artifact store, no provisioning, no build. It exercises
what the mesh *runs*; Phase 0 needs what the mesh *is*.
### `dev_up` — confirmed host-coupled
The claim in the work breakdown holds. `startDevProvider()` starts providers through the
host's user systemd instance and reads credentials from a host path
(`modules/hal/developer/tools/dev-env.ts:146-178`):
```ts
const unitName = `hal-module@${provider}.service`;
execSync(`systemctl --user start ${unitName}`, { timeout: 60_000, stdio: "pipe" });
const creds = resolveRunningProviderCreds(provider); // reads /services/<provider>/.env
```
It borrows the host. There is no mesh to stand up.
---
## 2. What is already portable
- **Node identity is one environment variable.** `resolveNodeName()` is
`process.env.HAL_NODE || hostname()`, lowercased (`modules/hal/sdk/src/mesh-config.ts:215`).
No file, no registration handshake, no host coupling.
- **Registration is a plain upsert.** `mesh_node_register` inserts into `nodes`
(`modules/hal/mesh/tools/index.ts:490-540`); mandatory fields are `name` and `user_name`.
- **The daemons are AMQP loops.** `hal/meshware` consumes `cmd.feature.{build,install,configure,start}`
(`modules/hal/meshware/daemon/src/cerebellum.ts:780-857`); `hal/coordinator` consumes
`event.gitea.push` and drives the cascade. Both are configured entirely by environment.
- **`hal/brain` in daemon mode already runs in a plain node image**, per `test/pipeline/`.
---
## 3. The obstacles, located
Four couplings stand between the daemons and a container. All are in code, none are
mysterious.
| # | coupling | location |
|---|---|---|
| 1 | meshware restarts itself via the host: `execFileSync("systemctl", ["--user","restart","hal-meshware.service"])` | `modules/hal/meshware/daemon/src/cerebellum.ts:826` |
| 2 | meshware restarts the broker via a hardcoded host path: `execFileSync("docker",["compose","-f","/services/lavinmq/docker-compose.yml","restart"])` | `cerebellum.ts:843-844` |
| 3 | **a module service *is* a systemd unit** — `hal-module@.service` runs `docker compose --project-directory /services/%i` | `modules/hal/meshware/systemd/hal-module@.service:10-11` |
| 4 | env generation writes `homedir()`-relative host paths — `~/.config/hal/env`, `~/.config/hal/modules/*.env`, `/services/*/.env` | `modules/hal/sdk/src/env-generator.ts:188-223`, `:538-568`, `:584-606` |
1, 2 and 4 are small: a guard and a configurable root. **3 is the architectural one**, and
it is the first open question below — in a container, "systemd unit that runs docker
compose" has no natural translation.
The bootstrap scripts add their own host assumptions — nvm for node resolution, `sudo
mkdir -p /services && chown`, and a `~/dotfiles` repository that is not in this repo
(`install.d/adopt.sh:165-170`) — but Phase 0 does not have to run them. A container image
can be built the way `test/pipeline/Dockerfile` builds one, bypassing the bootstrap path
entirely. That is a deliberate divergence to record: **the local mesh would not test the
bootstrap**, only the running mesh.
---
## 4. External services, and whether they can be local
| service | used for | local substitute |
|---|---|---|
| PostgreSQL | registry + pipeline state | yes — already the pattern in both harnesses |
| LavinMQ | all coordinator↔meshware messaging | yes — `cloudamqp/lavinmq`, already used |
| MinIO | per-feature artifact tarballs, `modules/{name}/{version}/{feature}.tar.gz` (`modules/hal/sdk/src/artifact-manager.ts:21,65`) | yes — `minio/minio`, needs only `REGISTRY_MINIO_*` |
| Gitea | source, push webhook, **and** the `@hal/*` npm registry | container exists, but heavy; see open question 2 |
| Docker registry | `docker push` for modules declaring `docker:` (`build-executor.ts:195-231`) | `registry:2`, or avoid modules that need it |
| Traefik | writes routing config at install; does not gate success | omit |
Only Gitea is awkward, and only because it carries two roles at once.
---
## 5. The fixtures
Phase 0.5 asks for at least one reproducible known fault. All three are reachable; they
differ sharply in cost.
### Fixture B — a provider deploy rotates a shared credential without fanning out
**Cheapest, best understood, and the root cause is still open.** Documented at
`troubleshooting/provision-adoption-rotates-live-credential`. The mechanism is two lines:
```ts
async provision(project, username, options) {
const password = generatePassword(); // ALWAYS a fresh password
```
and, in the adoption branch of `provisionDatabase`, an `ALTER ROLE ... WITH PASSWORD` that
rotates the live secret while updating only the provider's own `mesh_provisions` row
(`modules/postgres/tools/index.ts`). Every consumer sharing that role keeps a stale
password and fails permanently; the provider recovers alone, and that asymmetry is the
tell.
Reproduction needs one provider node and two consumer nodes — the minimum interesting
mesh. It re-fires on every provider deploy, so it does not need to be provoked, only
observed. Confirmation is a timestamp comparison, not a hash: the stored values are
`enc:v1:` with a random IV, so identical plaintexts hash differently.
Measured in production 2026-08-22: three nodes failing since 2026-08-20 14:15, 399 errors
each; the provider failed for 22 minutes and recovered by itself.
### Fixture C — a migration ships nothing while the pipeline reports success
Three independent silent-skip points, any one of which produces it:
1. **Not-applicable and succeeded are the same status.** `MigrationsHandler.detect()` is
`existsSync(join(moduleDir, "migrations"))` against the freshly cloned workspace
(`modules/hal/sdk/src/feature-handlers/migrations.ts:32-41`). If the directory is not
there — uncommitted, ignored, or a `module_path` that does not line up — the build
reports `success` (`cerebellum.ts:634-638`).
2. **Upload failure is a warning.** `uploadFeatureArtifact()` returns `{sha256: ""}` with a
`warn` when none of the listed files exist (`artifact-manager.ts:41-44`), and callers
catch and warn rather than throw (`cerebellum.ts:672-673`). The symmetric download
failure on the target node is also non-fatal (`artifact-manager.ts:93-98`,
`cerebellum.ts:426-431`, commented "Non-fatal: feature may work without artifact").
3. **Missing packaged files are skipped in silence.** `addFileEntries()` does
`if (!existsSync(abs)) continue;` (`module-builder-core.ts:134-135`) — an explicit
`package:` entry that is not on disk simply is not in the tarball.
This is the same class as `troubleshooting/flavors-never-packaged` (`flavors/` was never
staged, so a node kept its first copy forever) and is catalogued in
`troubleshooting/deploy-reports-transport-not-effect`, which counted **0 of 119 modules
implementing `verify`** — the stage that exists, is dispatched, has a working handler, and
would turn every one of these into a red pipeline.
### Fixture A — a credential rotation does not reach a running session
The most valuable and the most work: it needs a *running session* to rotate underneath,
which means the local mesh must be able to start one. The mechanism is understood — an
`EnvironmentFile` is read once and `process.env` is a snapshot — and it is why credentials
became files that get re-read. Defer it behind B and C.
**Recommendation:** B first. It needs three nodes and a provision, no build and no session,
and it is the one whose root cause is still open — so reproducing it locally has value
beyond proving the harness.
---
## 6. A claim that was checked and found false
The survey behind this document suspected that the live AMQP pipeline never writes
`deployments` or `node_modules.installed_version`, because the code that does so sits in
`installer-core.ts:1045-1126` on what is commented as the "CLI path".
Measured against production, 2026-08-22 17:30 UTC — deployments in the preceding 24 hours:
| node | deploys | newest |
|---|---|---|
| 1 | 24 | 17:26:17 |
| 2 | 27 | 17:22:22 |
| 3 | 47 | 17:25:54 |
| 4 | 26 | 17:22:23 |
The record is live on all four nodes and minutes old. **The claim is refuted.** Recorded
because the reasoning was plausible and someone will retrace it.
---
## 7. Questions
### Settled (jochen, 2026-08-22)
**Phase 0 is a development environment, not a fixture rig.** It is built as a supported
surface to work in daily, and it is what `dev_up` should become. The definition of done —
"demonstrated in the local mesh" — is therefore meant literally.
**The trigger is a real Gitea container.** The webhook relay is part of what is under test,
including the changed-file enrichment that exists because the webhook truncates at 20
commits (`modules/hal/gitea/tools/index.ts:136-195`). A synthetic `event.gitea.push` would
skip it. Gitea also carries the `@hal/*` npm registry, so the local mesh needs it twice
over either way.
### Open — blocking
1. **How does a node supervise a module service?** Today a module service is a systemd unit
running `docker compose` against `/services/<module>`
(`modules/hal/meshware/systemd/hal-module@.service:10-11`), which has no direct
translation inside a container. The question raised in response is the better one, and
is broader than Phase 0: **what would it cost to stop using systemd altogether and have
the mesh supervise its own services?** That is a mesh-level architectural question, not
a local-mesh implementation detail — if the answer is that the mesh should own
supervision, Phase 0 should not build a container-only workaround first. Under
investigation; findings will land in [`003-service-supervision`](../003-service-supervision/)
and, if it goes ahead, a decision record of its own.
2. **What replaces `~/.config/hal/env` and `/services/` inside a container?** A configurable
root keeps one code path; container-specific targets keep the host paths untouched. This
decides whether env generation gets a seam or a conditional.
3. **How faithful must the local mesh be to be trusted?** It will not run the bootstrap
scripts, and it will run one OS where the real mesh is heterogeneous by design
(`00-META/context.md`). Stating the divergence up front is what stops "it works
locally" from becoming its own class of silent failure. Now sharper, because a
development environment people use daily is trusted far more than a rig — and drifting
from production costs correspondingly more.
---
## References
- [`02-DECISIONS/0001`](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) — the decision this
phase unblocks
- [`03-DESIGN/00-work-breakdown.md`](../../03-DESIGN/01-to-be/00-work-breakdown.md) — Phase 0 tasks
and checkpoint
- `troubleshooting/provision-adoption-rotates-live-credential` — fixture B, root cause open
- `troubleshooting/deploy-reports-transport-not-effect` — the nine defects that shipped
green, and the unused `verify` stage
- `troubleshooting/flavors-never-packaged` — fixture C, previously seen in the wild
@@ -1,79 +0,0 @@
---
status: graduated
initiated: 2026-08-22
touches: [03-DESIGN/00-as-is/05-runtime-and-installation.md]
became:
- 02-DECISIONS/0005-the-node-host.md
- 03-DESIGN/01-to-be/05-the-node-host.md
---
# 003 — Who supervises a service
**No longer blocks Phase 0** (see below).
- **Initiated by:** jochen, 2026-08-22, in response to
[`002-local-mesh`](../002-local-mesh/analysis.md) open question 1
- **Areas touched:** every module shipping a `systemd/` directory (14), the
`hal-module@` template, `hal/sdk` feature handlers, `dev_up`, `log_tail` /
`systemd_journal`, the bootstrap scripts.
## The question
`002` asked how a containerised node runs a module service, given that a module service is
defined today as a systemd unit running `docker compose` against `/services/`. The response
was the better question:
> If it's possible to run systemd inside a container, that's the way to go I think. However,
> what would the cost be to step away from systemd to run our services and set it up in a
> different way? More hal mesh approach.
This effort answers the cost half. It does not choose.
## Summary of findings
- **The question splits in two**, and the halves have opposite answers. Supervising
`docker compose` stacks through systemd is largely **redundant** — 44 of 44 module compose
files already declare a restart policy, so Docker is already the supervisor. Supervising
HAL's **9 long-running Node daemons** is not redundant: `Restart=always` is currently the
only thing between a crash and a dead node.
- **The hard part is fate-sharing, not systemd.** meshware already cannot restart itself and
carries a documented workaround for it. Any mesh-native supervisor inherits that problem
recursively unless it sits outside the mesh's own process tree — at which point it is an
OS-level supervisor again, just reinvented.
- **There is a third option neither of us named**, and it is the one that also solves Phase 0:
run HAL's own daemons as containers, making Docker the supervisor for everything. Local and
production then have the same shape rather than a translation layer between them.
- **It cannot be all-or-nothing**, and ADR 0001 already says why: a human agent acts through a
shell and a desktop. Those parts are on the host by definition.
- One incidental finding: the automatic node rescue that documentation describes **does not
exist**. No unit declares `OnFailure=`, and nothing calls `hal-rescue.sh` on a timer.
Detail and costs in [`analysis.md`](analysis.md).
## What it became
*Closed 2026-08-28.* The decision this effort asked for was taken — and taken without citing it,
which is why the effort sat `active` for five days after being answered. Recorded here because
finding that is the point of a sweep.
**The third option is what the mesh adopted.** `Docker is the supervisor for everything` is
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md): the
host is a plain process on the machine and everything above tier 0 is a container. The substrate
bootstrap declares no service at all — it is package, container, action, container — so the
44-of-44 restart policies this effort counted are the supervision, exactly as it argued.
**Fate-sharing was the hard part, and it is solved the way this effort predicted.** It said any
mesh-native supervisor inherits the problem *unless it sits outside the mesh's own process
tree*. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) puts
the launcher there: it supervises the host as a child and shares no code with it, so a host that
cannot start is still recovered.
**It is not all-or-nothing, as this effort insisted.** A human agent acts through a shell and a
desktop, and those are on the host. So is the host itself — the one thing an init starts.
## What is not closed
**The automatic node rescue the documentation describes does not exist.** No unit declares
`OnFailure=`, and nothing calls the rescue script on a timer. That is a documented behaviour
which never happens, and it outlives this effort — filed as
[`04-ISSUES/008`](../../04-ISSUES/008-the-documented-node-rescue-does-not-exist/00-report.md)
rather than closed with it.
@@ -1,230 +0,0 @@
---
effort: 003-service-supervision
updated: 2026-08-22
---
# Who supervises a service — the cost of leaving systemd
Measured 2026-08-22. Every claim is a file location or a count.
---
## 1. What systemd actually does for HAL
**14 modules** ship a `systemd/` directory. The units divide cleanly into three kinds:
| kind | count | what it is |
|---|---|---|
| long-running daemons | 9 | `hal/brain`, `hal/meshware`, `hal/coordinator`, `hal/cortex`, `hal/env-sync`, `hal/file-share`, `hal/thoughts`, `noxflow/runtime`, `desktop_notifications` — all `Restart=always`, `RestartSec=10` |
| scheduled one-shots | 6 timers | docker-prune, docker-registry maintenance, claude-code sessions + usage, health, mailu cert-sync |
| the template | 1 | `hal-module@.service` — the per-module Docker lifecycle |
Only `mailu` is system-scope. Everything else is a user unit under
`~/.config/systemd/user`, auto-detected from directory presence — the module author names
the file and that filename *is* the unit name
(`modules/hal/sdk/src/feature-handlers/systemd.ts:31-42`).
The template is the piece `002` tripped over
(`modules/hal/meshware/systemd/hal-module@.service`):
```
ExecStart=docker compose --project-directory /services/%i --project-name %i up --remove-orphans --pull always
ExecStop=docker compose --project-directory /services/%i --project-name %i down
Restart=on-failure
StartLimitIntervalSec=120
StartLimitBurst=5
```
Systemd's roles, then: restart-on-failure, start at login, env-file loading, ordering,
per-module lifecycle, timers, and — via journald — **the only log store the 9 Node daemons
have** (`modules/hal/sdk/src/tools/log-tail.ts:50-53`).
---
## 2. The question splits in two, and the halves disagree
### For `docker compose` stacks, systemd is mostly redundant
**44 of 44** module `docker-compose.yml` files declare a container-level `restart:` policy —
`always` or `unless-stopped`, none missing. Docker's own daemon already restarts crashed
containers, independently of systemd.
`hal-module@.service` does not supervise the containers. It supervises the **`docker compose
up` foreground process**. It is a second layer on top of a restart policy that already
works. What it genuinely adds is narrower than it looks:
- one uniform verb for every module type — `systemctl --user start hal-module@X` rather than
remembering each module's compose invocation, relied on across a dozen call sites in
`meshware.ts:124-154`, `dev-env.ts:142-163`, `installer-core.ts`, `infra.ts:166`
- recovery when the `docker compose up --pull always` process itself dies — a failed image
pull, not a crashed container
- rate-limited restart (`StartLimitBurst=5`) so a broken stack does not spin
That is real, but it is a convenience layer, not a safety layer. **This half could go at
moderate cost.**
### For the 9 Node daemons, systemd is load-bearing
There is no alternative supervisor anywhere in the repo. No PM2, no forever, no nodemon, no
watchdog loop — all checked, zero hits. `Restart=always` is the only thing standing between
a crashed daemon and a dead node.
**This half is the actual question.**
---
## 3. The hard part is fate-sharing
The difficulty is not systemd. It is that **a supervisor must not share fate with what it
supervises**, and the codebase already has a scar from exactly this.
meshware cannot restart itself mid-request: killing the process before it ACKs the AMQP
message loses the message. So it defers its own restart by two seconds after closing the
connection (`modules/hal/meshware/daemon/src/cerebellum.ts:815-828`) — a commented
workaround for a problem that only exists because the thing being restarted is the thing
doing the restarting.
Any mesh-native supervisor inherits this recursively. Something has to be the outermost
always-alive process, and if it is written by the mesh, the mesh must supervise it, and so
on. The recursion only terminates at a process the mesh does not own. Today that is
systemd. **A "more mesh" supervisor that is itself a mesh process is not a smaller problem;
it is the same problem with a new name.**
This is the strongest argument for the status quo, and it is worth stating plainly before
looking at alternatives.
---
## 4. The option neither of us named
There is a third answer that terminates the recursion in something that is not systemd and
not written by us: **run HAL's own daemons as containers.**
Docker is already the outermost supervisor for 44 of 44 module stacks. It does not share
fate with the mesh. It already has restart policies, backoff, and a log store that
`log_tail` already speaks (`log-tail.ts:43-47` reads `docker logs` for Docker modules
today). Extending it from "the things the mesh runs" to "the mesh itself" is not new
machinery — it is applying machinery the mesh already trusts to one more case.
What that buys, beyond supervision:
- **Phase 0 stops being a translation.** A containerised node becomes the same shape as a
production node, rather than a local approximation with a systemd-shaped hole in it. The
divergence that `002` open question 3 worries about largely disappears.
- **`/services/` and `~/.config/hal/` stop being special.** Mounts, not host paths.
- **The GENESIS "dogfood everything" value gets easier**, not harder: the mesh's own
components would ship and run exactly like everything else it carries.
What it costs, honestly:
- **journald → docker logs** for the 9 daemons. `log_tail` already handles both, but
`systemd_journal` and the health checks that read unit state
(`modules/hal/mesh/health.sh:34-40`, `modules/hal/health/hal-health.sh:335-395`) would
need a container-aware path.
- **Boot start** becomes Docker's `restart: always` plus the Docker daemon being enabled at
boot — which is still one systemd unit, but the OS's own, not ours.
- **A container needs the host to be reachable** for anything that touches the node itself.
Some of these daemons exist precisely to write host files.
---
## 5. Why it cannot be all-or-nothing — and ADR 0001 already says so
Some of what runs under systemd today **cannot** be containerised, and the reason is
already in the domain model. ADR 0001:
> a non-human agent acts through a spawned session — a human agent acts through a shell or
> desktop
`hal/brain` has two modes (`modules/hal/brain/daemon/src/brainstem.ts:7-13`): a daemon mode
that is an AMQP relay, and a **cortex mode that is an MCP server over stdio for an
interactive Claude Code session**. The second is a human agent's modality. It runs in the
human's shell, on the human's node, against the human's `~/.claude`. Containerising it is
not a hard engineering problem, it is a category error.
The same holds for `desktop_notifications` and everything `hal/desktop-environment` touches.
So the line is not "systemd or not". It is:
| | belongs where |
|---|---|
| mesh daemons — meshware, coordinator, env-sync, thoughts, file-share, brain **in daemon mode** | supervisable by Docker; candidates to containerise |
| human-modality surfaces — brain **in cortex mode**, desktop notifications, desktop environment | on the host, by definition |
| module stacks | already Docker; systemd layer is the redundant part |
This split is not a compromise between the options. It is what the domain model implies,
and it is a decent sign that the model is doing work.
---
## 6. Options, with costs
| | option | cost | what it buys |
|---|---|---|---|
| **A** | **Keep systemd; systemd-in-container for Phase 0** | privileged containers, heavy images, slow iteration; local mesh keeps a shape production does not have | nothing changes in production; smallest change to the mesh |
| **B** | **Drop the `hal-module@` layer only** — let Docker's restart policies supervise stacks directly | reimplement uniform start/stop across ~12 call sites; lose rate-limited restart and pull-failure recovery | removes the redundant layer; does **not** solve Phase 0 on its own, since the daemons still need supervising |
| **C** | **Containerise the mesh daemons; Docker supervises** | container-aware `systemd_journal`/health; host access for daemons that write host files; the human-modality surfaces stay on the host regardless | terminates the fate-sharing recursion without writing a supervisor; makes local and production the same shape; Phase 0 becomes much less of a special case |
| **D** | **Write a mesh-native supervisor** | the fate-sharing recursion (§3), plus matching systemd's maturity — backoff, resource limits, clean SIGTERM (`noxflow/runtime` already depends on `TimeoutStopSec=60`) — on machines that are somebody's daily driver | most "mesh"; least justified by the evidence |
**D is the option the phrasing "more hal mesh approach" points at, and the evidence argues
against it.** Supervision is not a domain concern the mesh is better placed to solve than
the OS; the mesh's distinguishing feature is brokering capabilities, not restarting
processes. C gets the benefit D is reaching for — the mesh not depending on host-specific
init — without the recursion.
**C and B compose.** C is the one that pays for Phase 0.
Worth noting what is *not* in this table: the dozens of `systemctl` call sites that
configure the **host OS's own** units — NetworkManager, resolved, oomd, zram, docker.service,
fail2ban, sshd, zfs, across the vendor-firmware and power modules, `wireguard`, `sshd`, `zfs`
and others. Those are not HAL supervising itself; they are HAL configuring an Arch box. They
are out of scope for every option above and do not go away under any of them.
---
## 7. An incidental finding
Documentation describes an automatic node rescue: `hal-rescue.sh:20` states it is "triggered
automatically by `hal-health.timer` when hal-meshware is failed".
**It is not.** `hal-health.sh` contains no call to `hal-rescue.sh` (checked, zero matches),
and **no unit in the repository declares `OnFailure=`** (checked, zero matches). The only
real triggers are the manual `rescue_node` tool
(`modules/hal/sdk/src/tools/deploy-rescue.ts:77-92`) and running `install.d/rescue.sh` by
hand.
This is worth recording for two reasons. It weakens any argument that systemd-adjacent
self-healing is already wired — it is not. And it is another instance of the pattern this
whole refactor is about: **a documented mechanism that does not exist, believed because it
was written down.** `00-META/how-we-build.md` calls this out as a rule; here it is again,
found by grep.
---
## 8. Open question
**Which supervision model does the mesh adopt?** A, B, C, D or a combination.
**This no longer gates Phase 0.** When this was written, the local mesh was assumed to be
built from application containers, which forced the question — there is no natural way to
run an init system inside one. The decision of 2026-08-22 to build development nodes as
**system containers** (see [`03-DESIGN/01-end-to-end-testing.md`](../../03-DESIGN/01-to-be/01-end-to-end-testing.md))
removes that pressure entirely: a system container runs a real init, so the existing model
works unmodified and the lab needs no answer here to exist.
What remains is the question on its own merits, which is worth keeping open because the
evidence above still holds: option B removes a layer that 44 of 44 module stacks have already
made redundant, and option C would let the mesh stop depending on host-specific init. Neither
is urgent. Both are now cheap to *try*, because there is somewhere to try them.
Option D — a mesh-written supervisor — remains the one the evidence argues against, for the
fate-sharing reason in §3.
## References
- [`002-local-mesh`](../002-local-mesh/analysis.md) — the effort this came out of
- [`02-DECISIONS/0001`](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) — agent modality, which
decides what cannot leave the host
- `modules/hal/meshware/daemon/src/cerebellum.ts:815-828` — the self-restart workaround
- `modules/hal/meshware/systemd/hal-module@.service` — the per-module Docker lifecycle
- `modules/hal/sdk/src/feature-handlers/systemd.ts` — detection, install, start
@@ -1,67 +0,0 @@
---
status: graduated
initiated: 2026-08-22
touches: [03-DESIGN/00-as-is/01-mesh-and-transport.md, 03-DESIGN/01-to-be/01-end-to-end-testing.md]
became:
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 03-DESIGN/01-to-be/02-scenario-declaration.md
- 03-DESIGN/01-to-be/01-end-to-end-testing.md
---
# 004 — Reproducing the mesh network in a lab
- **Initiated by:** jochen, 2026-08-22 — *"the most difficult part of our VM setup will be
the networking part"*
- **Areas touched:** `modules/wireguard`, `modules/dnsmasq-app`, `modules/traefik`,
`modules/mesh-ca`, `node_accessors`, `nodes.site` / `nodes.underlay_addr`.
## Summary
The network is **entirely generated from mesh-DB rows by module hooks**. `install.d` performs
no network configuration whatsoever — no WireGuard, no DNS, no firewall. That makes a faithful
lab primarily a *data* problem rather than a networking problem, and means the lab exercises
the real code path instead of a reimplementation of it.
One constraint decides whether the lab works at all: the WireGuard endpoint rule tests the
underlay address against an RFC1918 regex to decide reachability. **A simulated public segment
addressed from RFC1918 space silently prevents the mesh from forming** — no endpoint is written
for the hub, so nothing can ever initiate. The simulated public segment must therefore use
TEST-NET-3 (`203.0.113.0/24`).
With that one substitution the lab reproduces the production topology exactly, including the
case that is hardest to get right: a node that is publicly *named* but sits behind NAT, whose
endpoint the hub can only learn from a handshake.
Detail in [`analysis.md`](analysis.md).
## Settled
**The lab issues its own certificates.** Public names are certified by an ACME server on the
lab's wan segment; `.internal` names keep the mesh CA. The lab preserves production's two-CA
split rather than collapsing it, because a single-CA lab would hide any bug living in that
split. It also makes the router's port forward load-bearing — HTTP-01 must reach the
published-but-NATed node on port 80, so a broken forward becomes a reproducible certificate
failure instead of a mystery.
Requires one change: `caServer` is not set on the reverse proxy today, so it defaults to the
public authority's **production** endpoint. It must become configurable, defaulting to
production so real nodes are unaffected.
## Open
*Closed 2026-08-25.* The lab is stood up. The topology this effort described raises, and the
substitution it turned on — a simulated public segment addressed from documentation space
rather than RFC1918 — is enforced by the declaration validator before anything is raised
rather than left as a thing to remember.
The certificate conclusion above is carried by
[`01-end-to-end-testing.md`](../../03-DESIGN/01-to-be/01-end-to-end-testing.md), which
specifies the lab's own ACME issuer on the public segment. It is **designed and not built** —
implementation state is a third axis, and the effort graduates on its conclusions, not on
their delivery.
One item leaves this effort without a home and is recorded here so it is not lost: the reverse
proxy does not set `caServer`, so it defaults to the public authority's production endpoint.
That is a fact about what runs today, not about the lab.
-203
View File
@@ -1,203 +0,0 @@
---
effort: 004-lab-network
updated: 2026-08-22
---
# Reproducing the mesh network in a lab
Established 2026-08-22 by reading the generating code and the live mesh DB. Every claim is a
file location or a queried row.
---
## 1. The network is data, not configuration
`install.d/mesh-init.sh` and `install.d/adopt.sh` perform **no network configuration at
all** — no WireGuard, no DNS, no firewall, no `/etc/hosts`. Every part of the network layer is
generated by module hooks from mesh-DB rows:
| layer | generated by | from |
|---|---|---|
| WireGuard interface + peers | `modules/wireguard/hooks/index.ts` (`postConfigure`) | `node_wg_keys`, `nodes.site`, `nodes.underlay_addr`, `module_env.WG_ADDRESS` |
| `.internal` name resolution | `modules/dnsmasq-app/hooks/index.ts` | mesh config peers → `internal_domain` + `wg_address` |
| public routing / vhosts | `modules/hal/sdk/src/vhost-gen.ts`, `feature-handlers/vhost.ts` | `vhosts:` manifests + `node_accessors` |
| internal TLS | `modules/mesh-ca/hooks/index.ts` | a singleton CA row in the mesh DB |
**Consequence:** a faithful lab is mostly a matter of writing the right rows. The network that
results is produced by the same code production runs, which is the difference between testing
the network and testing a model of it.
---
## 2. The constraint that decides whether the lab works
`modules/wireguard/hooks/index.ts:225-240` decides, per pair, whether to write an `Endpoint`:
```js
const isPrivate = (a) => /^(10\.|127\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(a);
if (coLocated && underlay) Endpoint = `${underlay}:${port}` // same LAN
else if (underlay && !isPrivate(underlay)) Endpoint = `${underlay}:${port}` // public
// else: no Endpoint — the peer must initiate, and we learn its endpoint from the handshake
```
A simulated public segment addressed out of RFC1918 space — `10.200.0.0/24`, say — makes the
hub's underlay test as **private**. No spoke writes an `Endpoint` for the hub. Nothing can
initiate. **No handshake ever occurs and the mesh silently never forms**, presenting as a
WireGuard fault rather than an addressing choice.
**The simulated public segment must therefore be `203.0.113.0/24`** — TEST-NET-3, reserved by
RFC 5737 for documentation, guaranteed never to route on the real internet, and not matched by
that regex. The code then treats it exactly as it treats a real hosting provider address.
This is the single most important fact in this document.
---
## 3. The topology being reproduced
The shape below is what a mesh of this kind looks like: one node with a routable address, one
publicly named but behind a household NAT, one stationary workstation, one that roams.
Addresses use the documentation ranges of RFC 5737 and RFC 1918 throughout.
| node | profile | site | underlay | WG | accessors |
|---|---|---|---|---|---|
| `anchor` | server | `dc` | `203.0.113.10` (routable) | `10.10.0.1/24` | `anchor.example` (public, primary) + `anchor.internal` (lan) |
| `home-server` | server | `home` | `192.168.1.135` | `10.10.0.2/24` | `home-server.example` (public, primary) + `home-server.internal` (lan) |
| `workstation` | workstation | `home` | `192.168.1.250` | `10.10.0.3/24` | `workstation.internal` (lan, primary) |
| `laptop` | workstation | `NULL` | `NULL` | `10.10.0.4/24` | `laptop.internal` (lan, primary) |
**Hub election is by convention, not by flag.** The hub is the node whose `profile='server'`
*and* whose `WG_ADDRESS` begins `10.10.0.1` (`hooks/index.ts:188`). A lab must assign
`10.10.0.1` to the node it intends as hub or there will be no hub.
**`site` drives direct peering** (`hooks/index.ts:197-203`). Two nodes with the same non-null
`site` peer directly with a `/32`; everything else routes through the hub's `/24`. A `NULL`
site means roaming and hub-only — deliberately, because WireGuard has no failover and a more
specific `/32` route to a dead endpoint blackholes rather than falling back.
The four interesting pairs, all of which the lab must reproduce:
| pair | behaviour | branch taken |
|---|---|---|
| anything → `anchor` | `Endpoint` written | underlay non-private |
| `home-server` ↔ `workstation` | direct peer, LAN endpoints, keepalive | co-located |
| **`anchor` → `home-server`** | **no `Endpoint`; learned from handshake** | not co-located, underlay private |
| `laptop` → anything | hub only, always initiates | `site` is `NULL` |
The third is the one worth building the lab for. The code comments at `hooks/index.ts:206-224`
record what it cost to get right: testing `profile === "server"` was tried and was wrong,
because a home-hosted node **is** a server yet is not publicly reachable — *"role does not
imply reachability; the address does."* An earlier version aimed the hub at that node's public
name, which hairpinned off the household NAT: 1.77 MiB sent, 0 B received, no handshake.
---
## 4. The lab
```
br-wan 203.0.113.0/24 TEST-NET-3 — non-private, so the code treats it as public
│
├── hub 203.0.113.10 profile=server site=dc WG 10.10.0.1
│
└── router VM 203.0.113.1 / 192.168.1.1
│ NAT, plus one forwarded port to reproduce a published-but-NATed node
│
br-lan 192.168.1.0/24 identical to production, same host addresses
├── a 192.168.1.135 profile=server site=home WG 10.10.0.2
└── b 192.168.1.250 profile=workstation site=home WG 10.10.0.3
c — attach to br-lan, or br-wan ("away"), or detach ("asleep")
underlay NULL profile=workstation site=NULL WG 10.10.0.4
```
Kept **byte-identical** to production: the LAN subnet and its host addresses, and the entire
WireGuard plan. Only the public segment is substituted, and only because it must be.
**The router earns its own VM.** It is what makes the published-but-NATed case real: that node
is reachable from outside only through a forwarded port, and the hub must learn its endpoint.
It also gives somewhere to break things — drop the forward and observe whether the mesh
notices or whether the public name simply stops working.
**Names.** A resolver on the wan side is authoritative for the public zone. `.internal` names
need nothing extra: `dnsmasq-app` generates them from mesh config on each node, and writes an
`/etc/hosts` block as a floor underneath, because a node must reach the mesh DB before its own
DNS exists.
---
## 5. What a node needs before any of this works
Rows in the mesh DB — `nodes` (`name`, `user_name`, `profile`, `site`, `underlay_addr`),
`node_accessors`, `module_env.WG_ADDRESS` (the hook hard-fails without it,
`hooks/index.ts:114-116`), and `node_modules` assigning at least `wireguard`, `dnsmasq-app`,
`mesh-ca`, and `traefik` where it serves.
On disk beforehand, because the node must reach the mesh DB before it can read any of the
above: registry database and object-store host and credentials, plus an npm token. This
ordering — contact the mesh before the mesh has configured you — is itself worth reproducing,
and is why the `/etc/hosts` floor exists.
Everything else is generated: the WireGuard keypair locally (the private key never leaves the
node; the public key is published to `node_wg_keys`), the peer list, the DNS records, the TLS
leaf.
---
## 6. Certificates — the lab issues its own
**Settled 2026-08-22: the lab runs its own ACME issuer.**
Public certificates use ACME **HTTP-01** via the reverse proxy, which requires genuine public
reachability, so an isolated lab cannot use the real issuer. Rather than forgo certificate
testing, the lab stands up an ACME server on its wan segment.
**The lab keeps production's two-CA split rather than collapsing it.** Production issues
public names from a public authority and internal names from the mesh CA; a lab with one CA
would hide any bug living in that split. So:
| | production | lab |
|---|---|---|
| public names | a public ACME authority | a test ACME server on the wan segment |
| `.internal` names | `mesh-ca` | `mesh-ca`, unchanged |
**A test issuer is the right shape, not a shortcut.** Purpose-built ACME test servers
deliberately vary their behaviour — validation timing, nonce handling, chain composition — to
expose assumptions a well-behaved authority would let pass. A lab CA that is *too* polite
tests less than the real thing, not more.
### It also exercises the port forward
HTTP-01 means the issuer must reach the node being certified on port 80. In the lab:
- the hub is directly reachable on the wan segment — straightforward
- **the published-but-NATed node is reachable only through the router's forwarded port**
So certificate issuance for that node passes only if the forward is correct. That is exactly
why its certificate works in production, and it makes "the forward is missing" a reproducible
failure rather than a mystery.
### Required change: `caServer` must be configurable
`modules/traefik/docker-compose.yml:17-19` sets the challenge entrypoint, the contact address
and the storage path — but **no `caServer`**, so Traefik defaults to the public authority's
*production* endpoint. Pointing the lab at its own issuer requires adding a `caServer` flag
fed by an environment value, defaulting to production so real nodes are unaffected and the lab
overrides it per node.
Worth noting independently of the lab: aiming at the production endpoint rather than a staging
one means every certificate experiment on a real node consumes production issuance quota, and
a retry loop can exhaust it for a week. The lab issuer removes that exposure.
---
## 7. Incidental finding: `scope:` is read by nothing
Several manifests declare `scope: public` on firewall rules — `wireguard`, `traefik`, `gitea`,
`mailu`, `qbittorrent`. It is **not part of the rule type** (`module-registry.ts:17-29`) and is
**referenced by no code** in the firewall path. Real scoping is done with `from:`, as
`modules/unifi/module.yml:52-93` does deliberately.
So a manifest can appear to restrict a port to the public scope and in fact restrict nothing.
This is the same shape as the rule in `00-META/how-we-build.md` — *an unenforced rule is
indistinguishable from a wrong one, and costs more, because people believe it.*
@@ -1,87 +0,0 @@
---
status: graduated
initiated: 2026-08-23
touches:
- 02-DECISIONS/0009-modules-and-the-graph.md
- 03-DESIGN/00-as-is/10-module-catalogue.md
- 03-DESIGN/00-as-is/02-modules-and-manifests.md
became:
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0009-modules-and-the-graph.md
---
# 005 — Which domains the catalogue groups into
[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) settles
that modules outside the platform core are grouped by domain rather than by single function,
and deliberately does not settle the list. This effort settles the list — and, first, tests
whether the premise survives measurement.
## What is being investigated
Eighty-nine modules sit outside the platform core. The question is which of them belong
together, and the method is evidence rather than intuition: **which modules actually change
together**, measured across the full history of the code repository.
## Why
The argument in ADR 0022 is that the catalogue's shape records what was installed rather than
what anything is for — that four modules constituting "how a node is reachable" have no
relationship the mesh can see, so a change to connectivity is made four times.
That argument is testable. If those modules genuinely change together, the grouping is
justified by more than tidiness. If they do not, the premise needs revising before a list is
drawn from it.
## What it touches
The catalogue's shape, the manifest, and — for one candidate grouping — the provisioning
reference itself, since a requirement names a provider **module**. Grouping providers would
change what a consumer names.
## Status
Measurement is done and is in [`analysis.md`](analysis.md). It **partly contradicts the
premise**, in a way that narrows the effort usefully:
- Non-platform modules overwhelmingly change **alone** — 10% of commits touch more than one,
and 50 of 89 never co-change with anything.
- Two clusters do exist. One of them, reachability, holds up as a domain.
- The other, the provisioned infrastructure providers, co-changes for a reason that argues
**against** grouping rather than for it.
The remaining work is the list itself, for the modules where grouping is justified, plus the
open questions below.
## What it became
*Closed 2026-08-28.* Three of the four questions are answered, and by records that did not cite
this effort — which is why it stayed open after being resolved.
**Whether provider modules group at all** — *no.*
[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md):
there is no `networking` thing to install, there are concrete modules named individually. Folders
assert relationships; edges record them. *Provider* stops being a category at the same time.
**Whether "group or leave" is even the right pair of options** — *it was not*, and that is the
useful finding. [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)
reframes it: things that change together share an **authority**, not a package. This effort's own
measurement is what that record rests on — reachability being the *only* place modules genuinely
co-change is why connectivity is a context and why nothing else needed one.
**What to do with the ~50 modules that co-change with nothing** — *nothing.* They are modules.
Grouping is a tag and a query over the graph, neither of which anybody keeps true by hand.
**What remains is a sequencing question, not a grouping one**, and it moves rather than closes:
*whether applications leave the monorepo before or after they group* is
[research 009](../009-migration/00-overview.md)'s, because it is about how to get from here to
there rather than about what the shape is.
## Open (superseded by the above) questions
| Question | Why it is open |
|---|---|
| Whether provider modules group at all, and if so what a consumer's requirement names instead of a module. | The provisioning reference is load-bearing; getting it wrong is expensive. Opinion and evidence in the analysis; not yet decided. |
| Whether applications group into domains now and leave the monorepo later as a unit, or leave first. | Decided in principle — group first, then split — but the migration order has real cost either way. |
| What to do with the ~50 modules that co-change with nothing. | The evidence gives no grouping signal for them at all. That may mean they are correctly sized already. |
| Whether "group or leave" is even the right pair of options. | [Research 006](../006-mesh-from-scratch/code-skeleton.md) finds a third fate — **absorbed into the node host**, ceasing to be a module at all — and argues it is the correct answer for the reachability cluster this effort measured. If so, the cluster this effort found is evidence for absorption rather than for grouping. |
-144
View File
@@ -1,144 +0,0 @@
---
effort: 005-domain-grouping
updated: 2026-08-23
---
# Which modules actually change together
## Method
Every commit in the code repository's main branch that touches the module catalogue, reduced
to the set of modules it touched. Platform-namespace modules are excluded — their
decomposition is settled by
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md). Modules that no
longer exist are excluded, because pre-rename names dominate the raw signal and describe a
catalogue nobody works in.
Commits touching more than eight modules are excluded from pair counting: a sweep across
forty modules says the sweep happened, not that the modules are related.
## Finding 1 — these modules overwhelmingly change alone
| Measure | Value |
|---|---|
| Non-platform modules today | 89 |
| Commits touching at least one | 312 |
| Commits touching two to eight together | 33 — **10%** |
| Modules that never co-change with anything | **50 of 89** |
Nine commits in ten touch exactly one module. More than half the catalogue has never been
edited alongside anything else.
This is the first thing a grouping proposal has to survive, and it is not what the premise
predicts.
## Finding 2 — two clusters exist
Everything above two co-changes falls into one of two groups.
**The provisioned infrastructure providers.** The densest cluster: the relational stores with
each other and with the cache, the broker, the object store and the image registry — a
strongly connected set, six to two co-changes per pair.
**Reachability.** The reverse proxy with the resolver, the firewall and the VPN; the firewall
with the intrusion filter. Three to two co-changes per pair.
Outside those: a torrent client with the VPN, twice. A login manager with a media player,
twice. Nothing else reaches two.
## Finding 3 — the provider cluster is manifest churn, not cohesion
This is the result that matters, and it is only visible by reading the commits rather than
counting them.
Every multi-provider commit is a **cross-cutting change to the machinery, applied N times**:
| Date | What the commit did |
|---|---|
| 2026-04-15 | modules own their tools — decentralised tool serving |
| 2026-04-24 | filesystem-driven feature detection; manifest boilerplate removed |
| 2026-04-27 | remove runtime dependencies that were only build dependencies |
| 2026-05-01 | hooks move into their own directory with their own package |
| 2026-05-05 | hooks renamed to a stage-and-feature convention; legacy install scripts removed |
| 2026-08-06 | verify **by shape in the SDK**, not by copying a script into every module |
| 2026-08-11 | bind declared volume paths that leaked an anonymous volume per deploy |
| 2026-08-23 | scope services to the local network and mesh; respect the profile gate |
**Not one of them is a change to what a database is.** They are all "the manifest contract
changed, therefore every manifest changed".
Two consequences follow.
Merging the providers into one module would not have prevented a single one of these commits.
It would have made the same edit land in one file rather than six — which is a diff-size
improvement, not a boundary.
And the history already shows the correct fix being applied, repeatedly and successfully:
*verify by shape in the SDK, not by copying a script into every module*. When the same edit
must be made in every provider, the answer that worked was **moving the concern into the
machinery**, not merging the modules that carry it.
Co-change here measures coupling to the manifest contract. It does not measure domain
cohesion, and using it as though it did would group the catalogue by which modules are most
boilerplate-heavy.
## Finding 4 — reachability holds up
The same reading applied to the reachability cluster gives a different answer. Most of its
twenty-five multi-module commits are the same machinery churn — but not all, and the
remainder are genuine:
| Date | What the commit did |
|---|---|
| 2026-07-29 | let the mesh's proxy coexist with another listener on the same port |
| 2026-08-03 | derive public split-DNS from node accessors so requests stop hairpinning |
| 2026-08-06 | firewall mesh-only by default, public by declaration |
Each is one intent — *change how a node is reachable* — landing across the proxy, the
resolver, the firewall and the VPN together. That is exactly the shape ADR 0022 describes, and
it is the only place in the catalogue where the measurement finds it.
The 2026-08-23 scoping commit is the sharpest case: it spans the reachability cluster **and**
two providers, because "which network is this exposed on" is a reachability question asked of
a database.
## What this means for ADR 0022
The record's principle stands, and its scope needs narrowing. Grouping by domain is:
- **Justified by evidence** for reachability. One intent, several modules, repeatedly.
- **Argued against by evidence** for the providers. The coupling is to the manifest contract,
and the demonstrated fix is to move the concern into the machinery.
- **Unsupported either way** for the fifty modules that co-change with nothing. Silence is not
evidence of independence — many are simply rarely touched — but there is no measured basis
for grouping them, and a proposal that groups them is drawn from intuition. It should say so.
The catalogue's shape is still wrong in the way the record describes. Measurement says
grouping is the right fix in fewer places than the record implies, and that a second fix —
moving cross-cutting concerns into the machinery — accounts for most of what looks like
grouping pressure.
## Recommendation on the provisioning reference
Asked directly, and stated as an opinion because it is not yet decided.
**Do not group the provider modules.** Three reasons, in order of weight:
1. **The evidence for grouping them is the wrong evidence.** Finding 3.
2. **A requirement names a provider module.** Grouping providers means a consumer names a
resource type and something else chooses the implementation. That is not a folder move; it
is implementation selection, a substantially larger design with its own failure modes, and
nothing currently asks for it.
3. **It would hide which implementation serves a requirement** — the one place the mesh most
needs to be explicit, and precisely the indirection ADR 0022 warns grouping causes.
A provider module is already exactly one purpose: it provisions one resource type. That is a
boundary, not an accident of installation.
## Still to do
- Draw the list for the cases where grouping is justified, and say plainly which entries rest
on measurement and which on judgement.
- Decide the ~50 silent modules: correctly sized, or unmeasured?
- Test the reachability grouping against the migration cost — the modules in it are among the
most-changed in the catalogue, so churn during a move is not hypothetical.
@@ -1,94 +0,0 @@
---
status: active
initiated: 2026-08-23
touches:
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 03-DESIGN/00-as-is/00-overview.md
- 03-DESIGN/01-to-be/00-work-breakdown.md
became: []
---
# 006 — The mesh designed from nothing
## What is being investigated
What the mesh would look like if it were laid out today, with the requirements known and none
of the accumulated shape — expressed as a **skeleton**: repositories at the root, modules
inside them, and whatever turns out to be the right leaf unit below that.
The deliverables are [`skeleton.md`](skeleton.md) — tiers, repositories and the four design
moves — and [`code-skeleton.md`](code-skeleton.md) — the tier test, what a module looks like on
disk, and where today's catalogue lands.
## Why
Every structural decision so far has been a **correction**: eight contexts replacing thirty-three
modules ([ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md)), domains
replacing single-function modules
([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)). A
correction inherits the frame of the thing it corrects, and two of the mesh's oldest problems
look unsolvable from inside that frame:
- **The bootstrap circularity.** The mesh needs a database, a bus, a registry and an identity
provider. Those are modules the mesh installs. The mesh cannot install them before it exists.
This has been worked around repeatedly and never designed away.
- **Participation requires privilege.** Everything assumes root on a machine whose packages and
services the mesh owns. A phone cannot participate on those terms, and neither can a machine
someone else administers.
Designing from nothing is a way to find out which parts of the current shape are requirements
and which are residue.
**And there is more residue than expected, from a knowable source.** This began as a
**dotfiles repository** — the first two days of history adopt dotfiles, add per-node dotfile
overrides, and introduce service symlinking with an ignore file. The flat one-directory-per-tool
catalogue, linking rather than copying, adoption of already-configured machines, per-node
overrides, and the desktop modules are all inherited from that, not chosen for a mesh. Recorded
in [`03-DESIGN/00-as-is/10-module-catalogue.md`](../../03-DESIGN/00-as-is/10-module-catalogue.md).
That makes this effort's question sharper than "what would we do differently": much of what
looks like design is a generalisation of *place files on my machines*, never revisited because
it was never stated as an assumption.
## The requirements this is designed against
Stated by the operator, recorded here so the skeleton can be checked against them rather than
against taste:
1. The mesh manages multiple computers — **full control**, through modules installed to nodes.
2. Mesh state lives in a **database**: which modules on which nodes, logs, configuration.
3. Configuration has **several touchpoints** — tool surface, web interface, others — all hosted
by the mesh itself.
4. **Connectivity** is core: every node reachable from every other over a shared overlay, some
nodes publicly exposed, firewalls configured.
5. The mesh **hosts applications** — and requires some of them itself. This is the circularity.
6. **Arch Linux only for now**; ideally any device, including phones, on lighter terms.
7. The end goal is to **operate an IT company** on it — development, design, deployment, full
circle, self-hosted. Personal cloud infrastructure.
8. **Agents make it self-improving and self-healing.**
9. It is **end-to-end testable on one machine**
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
## Status
A first skeleton exists, with four design moves that the current shape does not have. It is
`active` because two of them are unproven and one contradicts a record that is already
accepted.
**Finding worth stating up front:** [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md)
names nine bounded contexts and **none of them owns connectivity** — no overlay, no resolution,
no firewall, no ingress. Requirement 4 has no home in the accepted decomposition, while
[research 005](../005-domain-grouping/analysis.md) found reachability to be the *only* part of
the catalogue where modules genuinely change together under one intent. The skeleton adds it.
## Open questions
| Question | Why it is open |
|---|---|
| Does the record — the event log contexts integrate through — belong to the substrate or the control plane? | It is infrastructure by shape and domain by content. Placing it wrong reintroduces a circularity. |
| One repository per tier, or per context? | Already open from ADR 0001 as "catalogue destination — one repository or many". The skeleton assumes per tier and does not settle it. |
| ~~Does an unprivileged node earn a place in the inventory, or only a presence?~~ | **Answered 2026-08-25** by the operator: a node is a *managed machine inside the mesh*, not an unprivileged something — and a disconnected node is still a node, in a different situation. The question posed a class distinction; the answer is that there is none, and what varies is **state**. Recorded as [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md). |
| ~~Does absorbing overlay, filtering, packages, supervision and the container runtime make the host too large?~~ | **Answered 2026-08-25** — [`host-size.md`](host-size.md). Measured: the absorption is smaller than the machinery that already applies state, and eight of ten adapters already carry no dependency. The risk is not size but direction, and it is two modules wide. The claim survives with its scope corrected — the host carries one concern, *apply declared state on this machine*, of which the six are instances. Recorded as [ADR 0005](../../02-DECISIONS/0005-the-node-host.md), designed in [`05-the-node-host.md`](../../03-DESIGN/01-to-be/05-the-node-host.md). |
| ~~Four substrate services or five?~~ | **Answered conditionally**, which is the honest form — [`07-the-foundation.md`](../../03-DESIGN/01-to-be/07-the-foundation.md). The substrate is *what the control plane consumes and cannot grant itself*. The identity provider qualifies only if the control plane delegates authentication; if it authenticates natively it is an ordinary hosted service. The count follows from a decision not yet taken, and asserting four was asserting that decision. |
| Does `feature` survive? | The skeleton splits it in two and argues the conflation is what makes the delivery pipeline hard to reason about. Unproven. |
@@ -1,272 +0,0 @@
---
effort: 006-mesh-from-scratch
updated: 2026-08-23
---
# The code skeleton
[`skeleton.md`](skeleton.md) laid out tiers and repositories. This is the level below: what a
module looks like on disk, and — the question that forced a correction — where a given piece of
today's catalogue actually lands.
## The tier test
A decision procedure, so placement is answerable rather than argued. Ask in order; first match
wins:
1. **Does it apply state on a machine?** → tier 0, inside the host.
2. **Can the control plane *start* without it?** If *no* → tier 1, substrate. Note the verb:
*start*, not *function fully*. A capability the control plane loses without something is not
the same as a thing it cannot come up without — see "becoming self-hosting" below.
3. **Does it decide what should be true across nodes?** → tier 2, a control-plane context.
4. **Is it a way to talk to tier 2, holding no logic of its own?** → tier 3, a surface.
5. **Otherwise** → tier 4, a workload.
## Worked example — where postgres ends up
The obvious answer is "twice": once as the mesh's own database in the substrate, once as a
hosted database in the catalogue. That answer is wrong, and seeing why fixes something.
Run the test. *Can the control plane exist without a relational store?* No. **Postgres is
tier 1.** It lives once:
```
mesh-substrate/store/postgres/
```
There is no second copy in the catalogue, because the thing that differs between the mesh's own
database and a project's database is **not the module**. It is how that instance is brought up:
| | The mesh's own instance | A project's database |
|---|---|---|
| Brought up by | the host, from the pinned bundle, with no control plane present | the ordinary delivery and provisioning path |
| Declared in | `mesh-substrate/bundle.yml` | the consuming module's `requires:` |
| Exists because | the control plane cannot start without it | something asked for it |
Same module, two roles. **Tier is a property of the module** — what must exist before what — and
**the bundle is a property of the mesh's own instance.**
This is also legal under the dependency rule, which is worth checking rather than assuming: a
tier-4 workload that requires a database depends on tier 1, which points *downward*. The
inverse — substrate reaching into the catalogue for a module — would not be, and is the shape
the naive "twice" answer would have created.
The same reasoning places the rest of the substrate: the bus, the object store, the image
registry. Each fails step 2, each lives once, each is pinned. That is the whole substrate: four
services, and the deliberate absence of a fifth.
**The identity provider was raised as a boundary case and is settled: it is not substrate.**
The mesh does not require one — tier 2 authenticates its own callers natively, and an identity
provider is a service the mesh hosts like any other. The substrate is four services, not five.
The test earned its keep here by turning a vague unease into one answerable question — *does the
control plane delegate authentication?* — rather than a debate about how important identity
feels.
## Becoming self-hosting — the forge and the registries
Self-improvement means the mesh hosts the things it improves itself with: a forge, an image
registry, a package registry. The obvious worry is that these duplicate — a `mesh-gitea`
for the mesh and a `gitea` for everyone else. **They do not, and the reason is worth stating
carefully, because it is the same reason the bootstrap keeps failing today.**
### They are not substrate
Run the test with the sharpened verb. *Can the control plane start without a forge?* **Yes.** It
comes up, holds inventory, answers questions and manages nodes with the modules it already has.
What it cannot do is **change itself**. That is a capability, not a precondition.
So: forge, image registry and package registry are **tier 4 workloads**. One module each, in the
catalogue, exactly like the media server. There is no mesh-specific copy.
This is not a technicality. It buys a property worth having: **if the forge dies, the mesh keeps
running.** Nodes stay managed, services stay up, only self-modification stops. Putting the forge
in the substrate would make losing it fatal, for no gain.
### But delivery needs them — is that not an upward dependency?
It would be, stated naively, and that would break the one rule the whole skeleton rests on.
It is resolved the way the constitution already says to resolve it — **depend on abstractions,
not on concrete dependencies**. Tier 2's delivery context does not require *the forge module*.
It declares requirements:
| Delivery requires | Satisfied by |
|---|---|
| a source of record for module code | whichever module provides it |
| somewhere to publish images | whichever module provides it |
| somewhere to publish packages | whichever module provides it |
| somewhere to put build artifacts | the substrate's object store |
Tier 2 defines the requirement; tier 4 provides the implementation; the binding is data. The
dependency points **downward from the provider to the interface**, which is legal, and the
control plane never names a concrete module.
The mechanism for this already exists and is the mesh's most valuable one: **provisioning**. A
module declares what it provides; a consumer declares what it requires; the mesh binds them.
The only new idea is that **the control plane is itself a consumer** — it has requirements, and
they are satisfied the same way a workload's are.
That generalisation is significant enough to need its own study, and is not settled here.
### Self-hosting is a state the mesh reaches, not a precondition
This is the part today's mesh gets wrong, and it explains a recurring class of pain.
A first node comes up from **pinned external artifacts** — upstream images, by digest, carried
in the bundle. It has to: the mesh's own registry does not exist yet, and cannot. The mesh at
this point is running and manages nodes, and is not yet self-hosting.
Self-hosting is then **reached**: the forge is installed as an ordinary workload, the mesh's own
source moves into it, the registries come up, and delivery's requirements are re-bound from
external providers to internal ones. From that point the mesh builds and deploys itself.
Stated as a lifecycle:
```
pinned external artifacts ─► mesh runs, manages nodes
│
│ forge + registries installed as workloads
│ delivery's requirements re-bound
▼
mesh builds and deploys itself
```
**Today's mesh assumes the second state from the first moment.** Its source, its packages and
its images are all expected to be self-hosted before there is anything to host them — which is
why raising a first node needs a script that exists solely to paper over the impossibility, and
why that script is the least-exercised path in the system.
Making the transition explicit has a second benefit: it is reversible. A mesh whose forge is
broken can re-bind delivery to external providers and keep improving itself while it repairs
the forge. Today that escape hatch does not exist, because the dependency is not expressed
anywhere it could be changed.
### So, concretely
One `gitea` module. One image-registry module. One package-registry module. Each a tier-4
workload. The mesh's own instances are distinguished from any other instance **by what they are
bound to, not by being different modules** — precisely the same answer as postgres, arrived at
by the same test.
## The five fates of a module
Research 005 asked whether a catalogue module should be **grouped into a domain** or **leave
the repository**. Working the tier test across the catalogue surfaces a third answer that
neither option covers, and it is the most common one.
| Fate | Means | Examples from today |
|---|---|---|
| **Absorbed into the host** | It is not a module at all. It is part of what "managing a machine" means, and belongs in tier 0. | overlay membership, packet filtering, package management, service supervision, container runtime, filesystem management |
| **Substrate** | The control plane cannot exist without it. Pinned, host-applied. | relational store, bus, object store, image registry |
| **Control-plane context** | It decides something across nodes. | connectivity policy, inventory, delivery, provisioning, observability |
| **Workload module** | The mesh hosts it. Grouped per [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md). | media library, desktop session, collaboration tooling |
| **Leaves the repository** | A standalone application, per [ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md). | the applications identified in research 005 |
**The first fate is the finding.** Research 005 measured the reachability cluster — proxy,
resolver, firewall, overlay — as the only place in the catalogue where modules genuinely change
together under one intent. The skeleton explains *why*: they are not four modules that ought to
be one domain module. They are four facets of one thing the host should own, currently
expressed as modules because a module was the only unit available.
Under this skeleton the overlay module and the firewall module **stop existing**. The host holds
membership and applies filtering; tier 2 decides the policy; the swappable backends stay
modules. That is a different and better answer than grouping them, and it was not visible from
inside the current frame.
It also partly answers research 005's other open question — the fifty modules that co-change
with nothing. Several are host concerns rather than domains: package management, container
runtime, filesystem tooling. Silence was the right signal after all; the wrong conclusion was
that grouping was the only available fix.
## What a module looks like on disk
Per [`skeleton.md`](skeleton.md) Move 4, a module declares **parts** — independently selectable
pieces of desired state — and produces **artifacts** — things built once per version.
```
<module>/
module.yml identity, what it provides, what it requires,
which host profiles it can land on
parts/
service/ desired state: container, volumes, exposure
provisioner/ how it grants its resource to consumers
migrations/ its own persistent state
tools/ capabilities it contributes
artifacts/
<name>/ source for something built and published
```
A module with no artifacts consumes an upstream image and builds nothing. A module with no
parts is not a module.
Worked through for the substrate's relational store:
```
mesh-substrate/store/postgres/
module.yml provides: database · profiles: [managed]
parts/
service/ the container, its volume, its network exposure
provisioner/ grants a database and role to a consumer
migrations/ none — it holds no state of its own
artifacts/ none — upstream image, pinned by digest in bundle.yml
```
## The tree, at file level
```
mesh-host/ TIER 0
cmd/host/
internal/
apply/ reconcile declared state
inventory/ what this machine is and can do
link/ outbound connection to the control plane
overlay/ membership: address, keys, tunnel
filter/ packet filtering from tier-2 policy
packages/ package management
services/ supervision
containers/ container runtime
store/ embedded local state
profile/ managed · user · edge
substrate.lock pinned tier-1 descriptor
mesh-substrate/ TIER 1
bundle.yml the pinned set, by digest
store/postgres/
bus/<broker>/
objects/<object-store>/
images/<registry>/
mesh-control/ TIER 2
record/ the event log contexts integrate through
inventory/ nodes · modules · assignments · versions
config/ settings · secrets · derivation
connectivity/ addresses · resolution · exposure · filtering · certificates
provisioning/ grants between modules
delivery/ source → artifact → node
observability/ health · logs · metrics
identity/ agents · humans · services · authorisation
work/ tasks · workflows · runs
knowledge/ memory · documents · retrieval
api/ the one interface surfaces speak to
mesh-surfaces/ TIER 3
tools/ web/ cli/
mesh-catalog/ TIER 4
<domain>/<module>/ layout as above
mesh-lab/ mesh-sdk/ hq/ (company-scoped — ADR 0019)
```
## What this does not settle
- **The identity boundary case.** Four substrate services or five.
- **Where the record lives.** Still the open question from
[`00-overview.md`](00-overview.md), and the tier test does not resolve it: the record is
needed by tier 2 and is *of* tier 2, which is exactly the shape that produces a circularity.
- **Whether absorbing into the host makes the host too large.** Six internal concerns is already
a lot for a binary whose whole argument is that it has no dependencies. The counter-argument
is that each is small and none can be optional — but this is the skeleton's biggest unproven
claim, and it should be tested by writing the host's interface before anything else.
- **The migration.** Nothing here says how today becomes this.
@@ -1,126 +0,0 @@
# Is the host too large?
The skeleton absorbs overlay membership, packet filtering, package management, service
supervision, the container runtime and filesystem management into tier 0, and
[`00-overview.md`](00-overview.md) calls this *"the skeleton's biggest unproven claim. A binary
whose whole argument is that it has no dependencies now carries six concerns."*
This is that claim, measured.
## Method
Against `origin/main` of the monorepo at 2026-08-25 — read through git refs rather than a
checkout, which sits on a feature branch 1031 commits behind with uncommitted work.
For each module that implements one of the six concerns: how much code it is, and **what it
depends on to do its job**. The second question turned out to be the one that matters.
## Finding 1 — by size, the concern is misplaced
| Absorbed into the host | Lines |
|---|---|
| `dnsmasq-app` | 706 |
| `traefik` | 426 |
| `ufw` | 385 |
| `mesh-ca` | 353 |
| `wireguard` | 313 |
| `incus` | 285 |
| `package-manager` | 118 |
| `zfs` | 67 |
| `fail2ban` | 60 |
| `docker-app` | 42 |
| **total** | **2 755** |
| Machinery that already applies state on a node | Lines |
|---|---|
| `hal/meshware` | 1 829 |
| `hal/env-sync` | 626 |
| `hal/config-sync` | 604 |
| **total** | **3 059** |
**Everything being absorbed is smaller than the machinery that already exists to apply it.**
The six concerns are not six subsystems; they are ten thin adapters averaging 275 lines, most
of which is rendering a config file and running a command.
The host is not a new large thing. It already exists, spread across three core modules, and
the absorption adds less code than those three already contain.
Size is therefore the wrong axis, and the open question asked about the wrong thing.
## Finding 2 — the real risk is direction, and it is narrow
What each adapter needs in order to decide what to write:
| Module | Gets its inputs from | Applier only? |
|---|---|---|
| `ufw` | `~/.hal/modules/*/module.yml` on the local disk | yes |
| `dnsmasq-app` | `process.env.DNSMASQ_*`, derived centrally by `env-sync` | yes |
| `fail2ban`, `mesh-ca`, `package-manager`, `docker-app`, `incus`, `zfs` | local state and declared env | yes |
| `wireguard` | **direct `pg` connection** — `nodes`, `node_accessors`, `node_wg_keys`, `module_env` | **no** |
| `traefik` | **direct `pg` connection** — `nodes`, `mesh_ca` | **no** |
**Eight of ten are already pure appliers.** They receive derived state and put it on the
machine. Absorbing those into tier 0 moves no dependency at all — it moves code that already
has none.
**Two reach upward.** `wireguard` and `traefik` open a connection to the control plane's
database and compute their own configuration from it. Absorbing them *as they are* would put a
Postgres client and knowledge of the mesh schema inside tier 0 — an upward dependency, which
is precisely what
[the tier rule](skeleton.md) forbids and what the entire bootstrap argument rests on.
So the danger in the absorption is real, and it is two modules wide rather than six concerns
wide.
## Finding 3 — the split has already been happening, unnamed
`dnsmasq-app` is the same *kind* of module as `wireguard`: it needs every node's addresses and
names. It does not query for them. Its own comments record why:
> *"the mesh DB already holds [this] in `node_accessors` and the WireGuard address, duplicated
> by hand on all four nodes. Renaming the namespace then meant editing four override rows
> nobody [knew about]."*
and
> *"now derived from `node_accessors`"*
That is one module having already made the move the skeleton proposes — deciding centrally,
applying locally — for the ordinary reason that hand-duplicated state went wrong. Nobody named
it as an architectural direction; it was reached by fixing a bug.
Eight of ten adapters are on the far side of that migration. Two are not.
## What this means for the open question
**The absorption is not a move. It is a split, and it is mostly already done.**
The question "does the host become too large" assumed six concerns would arrive whole. They do
not. Each divides:
- **deciding** — what this node's overlay, names, exposure and filtering should be, which needs
every other node and therefore belongs in tier 2;
- **applying** — putting that on the machine, which needs root and locality and therefore
belongs in tier 0.
Tier 0 absorbs the applying. That is ~2 755 lines today, most of it already dependency-free,
against 3 059 lines of apply machinery the host needs regardless.
**The claim survives, with its scope corrected.** The host does not carry six concerns; it
carries one — *apply declared state on this machine* — of which the six are instances. That is
the skeleton's own tier-0 test (*"does it apply state on a machine?"*) applied to itself.
## What remains open
- **The two unsplit modules are the two hardest.** Overlay needs every node's key, address,
site and endpoint reachability; the proxy needs certificates and every node's exposed names.
These are where "derive centrally, apply locally" is most work, and neither has been done.
The measurement says the design is right; it does not say the migration is cheap.
- **Six concerns is still six vocabularies.** Absorbing them adds no dependencies but does add
surface: the host must know what a WireGuard peer, an nftables rule, a package, a unit, a
container and a dataset *are*. Nothing here measures that cost, and it is the residue of the
original worry.
- **What the host must carry versus what it must find.** The host manages `wg`, `nft`,
`pacman`, `docker`; it does not contain them.
[Issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md) is
exactly this question and is unresolved.
@@ -1,281 +0,0 @@
---
effort: 006-mesh-from-scratch
updated: 2026-08-23
---
# The skeleton
Repositories at the root, modules inside them, parts at the leaf. Four tiers, and a dependency
rule that only points downward.
## What a tier is
A tier answers one question: **what has to exist before this can exist?** It is not importance,
and it is not a layer in the networking sense. It is bootstrap order, made explicit.
Walk a bare machine to a running mesh and the tiers fall out of the story:
```
bare machine
│ one command lands ONE binary. Nothing else exists. TIER 0 host
│
│ it reads a pinned file it already carries and raises a
│ database, a bus, an object store and an image
│ registry — locally, alone. TIER 1 substrate
│
│ on those, the mesh's brain starts: which nodes exist,
│ what runs where, what is reachable. TIER 2 control plane
│
│ ways to talk to that brain. TIER 3 surfaces
│
└ everything the mesh then carries. TIER 4 workloads
```
**Dependencies point only downward.** The substrate never references the control plane. That
one constraint is the entire bootstrap answer, because it guarantees there is always a place to
start.
Why it earns its keep here: **the current mesh violates this, and that is the circularity that
keeps recurring.** The mesh database is a module; modules are installed by the delivery
pipeline; the pipeline needs the database. No order works, so a first-node script exists to
paper over it, and every later substrate change has to pretend the problem is not there.
A tier is not a repository and not a bounded context. Those are different cuts: a context says
*who owns this concept*, a tier says *what must already be running*.
## The tree
```
mesh-host/ TIER 0 — the only thing ever installed by hand
apply/ reconcile declared state on this machine
inventory/ what this node is, has, and is capable of
link/ the single outbound connection to the control plane
store/ embedded local state — authoritative while disconnected
profile/ capability detection: managed · user · edge
substrate.lock pinned tier-1 descriptor, appliable with no mesh present
mesh-substrate/ TIER 1 — declarations only, no logic of its own
store/ relational state
bus/ commands and events
objects/ blobs and build artifacts
images/ container images
bundle.yml the pinned set tier 0 can raise alone
mesh-control/ TIER 2 — the control plane
record/ the event log every context integrates through
inventory/ nodes · modules · assignments · versions
config/ settings · secrets · derivation onto nodes
connectivity/ overlay · resolution · exposure · filtering · certificates
provisioning/ resource grants between modules
delivery/ source → artifact → node
observability/ health · logs · metrics · alerts
identity/ agents · humans · services · authorisation
work/ tasks · workflows · runs
knowledge/ memory · documents · retrieval
api/ the one interface every surface speaks to
mesh-surfaces/ TIER 3 — thin; no logic lives here
tools/ the agent-facing tool surface
web/ the operator-facing interface
cli/ the shell-facing interface
mesh-catalog/ TIER 4 — what the mesh hosts
<domain>/ grouped per ADR 0022, list per research 005
mesh-lab/ the whole mesh, disposable, on one machine
mesh-sdk/ contracts shared across tiers — types, not behaviour
hq/ company-scoped, not a mesh repository — ADR 0019
```
## The dependency rule
**A tier may depend only on tiers below it.** Substrate never references the control plane.
The control plane never reaches into a node except through the host. A surface holds no logic
a second surface would have to reimplement.
This is the whole of the bootstrap answer, and per this repository's own rule it must say how
it is checked: a dependency-direction lint in the build, failing on an upward import. A tier
rule enforced by intention is the same as no tier rule — that is
[ADR 0010](../../02-DECISIONS/0010-delivery.md) applied to architecture.
## Move 1 — the substrate is applied, not delivered
**The problem.** The mesh needs a database, a bus, an object store and an image registry.
Today those are modules, and modules are installed by the delivery pipeline, which
needs the database and the bus. The first node is therefore raised by a special script that
exists only because of the circularity, and every later change to the substrate has to pretend
the circularity is not there.
**The move.** The host can apply a declaration without anyone telling it to. The substrate is
a **pinned bundle** the host carries: a fixed, versioned, self-contained descriptor of the
four services and nothing else. Raising a first node is `host apply substrate.lock` — not a
special path, just the ordinary one with no control plane on the other end.
The circularity disappears rather than being worked around: **the substrate is applied by tier
0, the mesh is delivered by tier 2, and they are different mechanisms on purpose.**
The price is real and should be named: the substrate is upgraded by bumping a pin and
re-applying, not by the pipeline. It gets less machinery than everything else — no per-node
selection, no provisioning, no fan-out — and that is the point. Five services justify a
simpler mechanism than a hundred.
## Move 2 — the host is one binary with capability profiles
**The problem.** Everything assumes root on a machine whose packages, services and network the
mesh owns. A phone cannot offer that, and neither can a work laptop. The current answer would
be a lightweight fork, which means two implementations and one of them rotting.
**The move.** One host binary, and a **profile** it detects rather than is told:
| Profile | Can | Typical |
|---|---|---|
| `managed` | packages, services, network, filesystem — the full surface | a machine the mesh owns |
| `user` | user-level services and tools; no package or network management | a shared or administered machine |
| `edge` | report presence, relay, expose a tool surface; hold nothing | a phone |
A module declares which profiles it can land on. Assignment to an incapable node fails at
declaration time, not at deploy time — a phone is not a machine that fails to install a
firewall; it is a node the firewall module cannot be assigned to.
This makes the phone case a **capability question rather than a platform question**, which is
what keeps it from becoming a second implementation. It also removes the current unstated
assumption that every node is equivalent — already false, and today handled by remembering.
## Move 3 — connectivity becomes a context
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) names nine contexts
and none of them owns the overlay, the resolver, the firewall or the ingress. `config` owns
PKI, which is the closest thing, and it is not close.
Meanwhile [research 005](../005-domain-grouping/analysis.md) measured the whole catalogue and
found that reachability is the **only** place where modules genuinely change together under one
intent — the proxy with the resolver, the firewall with the overlay, repeatedly, because *how a
node is reachable* is one question asked in four places.
So the evidence and the gap point the same way. `connectivity` owns:
- the overlay every node joins, and the addresses on it
- name resolution, internal and public
- exposure — which services answer from outside, on which names
- filtering — what may reach a node at all
- certificates for both name spaces
This is an addition to an accepted record, so it is a decision, not a drafting choice. It
belongs in a new record that extends ADR 0001 the way
[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) does —
not written here.
### Where the networking actually lives
A context is an authority, not a running thing, so naming one does not say who brings the
overlay up. That splits three ways, and the split is the design:
| Concern | Tier | Why there |
|---|---|---|
| **Overlay membership** — this node joins, holds an address, keeps the tunnel up | **0, in the host** | Everything cross-node needs it *before* the substrate is reachable from elsewhere. A module cannot provide it, because installing a module is itself a cross-node operation. |
| **Policy** — who holds which address, what resolves, what is exposed, what is filtered | **2, `connectivity`** | Bookkeeping and authority. It decides; it runs nothing. |
| **Machinery** — resolver, reverse proxy, firewall backend, certificate issuance | **4, modules** | Swappable, and not every node needs them. A node without a reverse proxy is still a node. |
There is a second circularity hiding here, and it has to be closed explicitly: **the host's link
to the control plane does not run over the overlay.** If it did, the overlay would have to be up
before the host could be told how to join it. The link is ordinary outbound internet to a public
endpoint; the overlay carries node-to-node traffic only.
Joining is therefore: host lands → links out with a join token → control plane returns an
address and keys → host raises membership → the substrate on other nodes becomes reachable.
This also makes the `edge` profile honest rather than special-cased. A phone can hold overlay
membership in userspace without privilege, and cannot run the machinery. That is the profile
distinction doing its job.
## Move 4 — `feature` splits in two
The invitation was to check whether the concept survives. It does not, in one piece.
Today a **feature** means both *a thing built once* and *a thing selected per node*, and the
delivery pipeline is hard to reason about precisely because those have different cardinality
and one word ([ADR 0010](../../02-DECISIONS/0010-delivery.md)
is the pipeline half of the same confusion).
Split it:
| Concept | Is | Cardinality |
|---|---|---|
| **artifact** | something built and published — an image, a bundle, a package | once per module version |
| **part** | an independently selectable piece of a module's desired state | chosen per node, per assignment |
A module declares desired state in parts, and produces artifacts. An assignment names the node
and the parts. Delivery builds artifacts once and applies parts per node — and the two words
now carry the two cardinalities that the pipeline already has.
This keeps what features are genuinely for — per-node opt-in of *some* of a module, which the
work breakdown already calls for — and drops the conflation that makes the current model
confusing.
## What the mesh is, versus what it hosts
Tiers 0–3 are the mesh. Tier 4 is everything it carries, and the boundary is stated by
requirement rather than by taste: **a module is part of the mesh if removing it stops the mesh
managing nodes.** A media server does not. A relational store does — which is why it sits in the
substrate and not the catalogue, despite being, in every other respect, an application like any
other. An identity provider, notably, does **not**: the control plane authenticates its own
callers, so identity is a hosted service like the media server.
That test also settles the IT-company goal without a special category. Development, design and
deployment tooling are **workloads** — tier 4, hosted, provisioned, delivered like anything
else. The mesh does not grow a "company" feature; it hosts the tools a company runs on, and
the fact that it runs its own development on them is dogfooding, not architecture.
## What agents are, structurally
Self-improvement and self-healing are not a tier. Agents are participants
([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)) that hold identity in
tier 2, act through tier 3 like any other caller, and run as workloads in tier 4.
This matters for one reason: **an agent must not have a privileged path**. Anything an agent
can do to the mesh, a person can do through the same surface, and anything it cannot express
through the tool surface is a gap in the surface rather than a reason for a back door. Self-
healing built on a private channel is unreviewable, and would be the one part of the mesh with
no human checkpoint.
## How this is tested
The lab ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)) raises the
tree above on one machine: virtual machines as nodes, a real overlay between them, the real
substrate bundle, the real control plane, the real delivery path.
The tier rule is what makes that affordable. A scenario needing only tiers 0 and 1 is one
virtual machine and a pinned bundle — which is also, exactly, the bootstrap path. **The
hardest thing to test becomes the cheapest scenario to run**, and the first-node path stops
being the one thing nobody exercises until it breaks.
## What this skeleton does not answer
- Where the record lives. It is infrastructure by shape and domain by content, and putting it
in the substrate risks recreating a circularity in the one place the design just removed one.
- Whether tier 2's contexts are one repository or several. Open from ADR 0001 already.
- Whether an `edge` node is in the inventory or merely present — which decides whether "node"
is one concept or two.
- The migration. Nothing here says how today's mesh becomes this, and the skeleton is worth
little until that is costed.
## A naming near-miss, recorded
The tier-0 binary was first called `mesh-agent`, because "node agent" is the reflex everywhere
else in the industry. That is wrong here, and wrong in the specific way
[`how-we-build.md`](../../00-META/how-we-build.md) §4 exists to catch: **Agent** is a
first-class concept in this mesh — a participant, some of whom are human, holding identity and
memory ([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)). One document
carried both meanings.
It is the same failure as the anatomy naming in the current runtime: an evocative domain word
pointing at infrastructure.
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) supplies the fix in
its own title — *the mesh brokers capabilities; nodes host; agents think.* Three verbs, three
components: the control plane **brokers** (`mesh-control`), the tier-0 binary **hosts**
(`mesh-host`), the participant **thinks** (`agents`, untouched).
`mesh-node` was the alternative and was rejected: *Node* is the aggregate in the inventory — the
record of a machine — while the binary is what runs on it and does the hosting.
@@ -1,51 +0,0 @@
---
status: active
initiated: 2026-08-23
touches:
- 03-DESIGN/00-as-is/03-provisioning.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
became: []
---
# 007 — Provisioning as the mesh's core mechanism
## What is being investigated
Provisioning is the mechanism the whole mesh rests on: a module declares what it needs, and the
mesh makes it exist, generates the credential, records the grant, and puts the values where the
module will read them. [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)
calls it the mesh's core concern rather than its plumbing.
[Research 006](../006-mesh-from-scratch/code-skeleton.md) then asks it to carry **more**: the
control plane becomes a consumer with its own requirements — a source of record, an image
registry, a package registry — satisfied by the same mechanism. That generalisation is only
safe if the mechanism is sound, and the as-is record says it is not, in named ways.
## Why now
Four weaknesses are already documented in
[`03-DESIGN/00-as-is/03-provisioning.md`](../../03-DESIGN/00-as-is/03-provisioning.md), each
observed rather than theorised:
- **Rotation has no fan-out.** A shared credential can be rotated without telling the peers
holding the old one. This has locked the mesh out of its own broker.
- **A grant is not a check.** The record says a resource was provisioned. Nothing verifies it
still exists, still has that credential, or is reachable from where the consumer runs.
- **A frozen password outlives its generation.** A generated secret written once diverges from a
persistent data directory initialised earlier, and presents as an authentication error.
- **No requirements is indistinguishable from provisioning that did not run.** A module that
declares nothing skips the stage, which is correct, and looks identical to failure.
Generalising a mechanism with these properties to the control plane's own dependencies would
make each of them fatal rather than annoying.
## The questions
| Question | Why it matters |
|---|---|
| What does a **grant** mean, exactly — a record that a resource was created, or a claim about the world that is continuously reconciled? | The difference between the current model and one where "provisioned" is checkable. Almost every weakness above is a symptom of the first answer. |
| How is a credential **rotated** with fan-out to every holder? | The mechanism grants easily and regrants not at all. This is the most damaging gap and it has taken the mesh down. |
| Can the **control plane** hold requirements, and what satisfies them before anything is installed? | The generalisation research 006 needs. Ties directly to the self-hosting transition. |
| What happens when a requirement **cannot** be satisfied — no provider, provider on an unreachable node, provider not yet installed? | Today this is silent or a stall. It should be a stated, visible state. |
| Does a requirement belong to a **module** or to one of its **parts**? | Research 006 splits `feature` into artifact and part. A part-scoped requirement means a database is not provisioned where the part that needs it is not installed. |
@@ -1,101 +0,0 @@
---
status: graduated
initiated: 2026-08-23
touches:
- 03-DESIGN/00-as-is/04-delivery.md
- 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0010-delivery.md
- 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
became:
- 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0010-delivery.md
---
# 008 — The coordinator: a change checked in becomes a deployed state
## What is being investigated
The mesh's own continuous delivery: a change is committed, and the mesh ends up in the state
that change describes — across every node the change touches, with a verdict that says whether
it worked.
The coordinator is what orchestrates that, and it is the mesh's most consequential machinery:
everything reaches every node through it.
## Why now
The as-is record ([`03-DESIGN/00-as-is/04-delivery.md`](../../03-DESIGN/00-as-is/04-delivery.md))
names problems that are structural rather than incidental:
- **A green pipeline proves transport, not effect.** The stages report that a message was
dispatched and accepted, which is not the same as the thing running, correct, or present.
This is the mesh's single most consistent failure shape.
- **Detection is the most fragile input.** A merge that creates no pipeline, with nothing saying
so, is the characteristic bad outcome — and it has happened for reasons unrelated to the
change.
- **The fan-out point is asymmetric.** The build node has already passed two silos when work
fans out, and code that knew only about the first parked it forever while every other node
deployed cleanly.
- **There is no end-to-end coverage.** The harness has not built since 2026-06-04
([`04-ISSUES/005`](../../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)).
Research 006 adds a requirement the current design does not have: the coordinator must work
**before the mesh is self-hosting**, when source and artifacts come from outside, and keep
working across the transition to self-hosted providers.
## What it became
*Closed 2026-08-28.* All six questions are answered, by two records, and the second exists
because the first was honest about what it did not fix.
**Does the coordinator dispatch stages, or converge nodes on a declaration?** — *Converge.*
[ADR 0010](../../02-DECISIONS/0010-delivery.md): a pipeline ends when the
declaration is updated, and the host applies it and reads back — so the reporter is the applier.
**Does the three-silo split survive?** — *Yes, with the third redefined.* The cardinality
observation holds; the third silo is not a stage any more.
**How does a change become a pipeline, reliably?** — *It does not become a pipeline at all.*
[ADR 0010](../../02-DECISIONS/0010-delivery.md) applies 0058's
move one level up: the control plane holds what source exists and what has been built, and builds
the difference. **An event makes it fast; nothing makes it necessary.** The failures this effort
catalogued — a truncated commit list, a broken path match — become latency rather than silence.
**What is a deployed state?** — *Two comparisons, not an event.* Does every node's reported state
match what is declared, and is what is declared built from current source? A milestone can be
claimed by something that did not check; a comparison cannot.
**What produces a verdict, and what is it about?** — *An artifact, and it gates eligibility.* The
mesh must not converge onto something broken, so an artifact may be declared only once the lab
has judged it fit. Sharper than the question expected: a verdict is a property an artifact has,
not a report about a run.
**How does delivery work before self-hosting?** — *It mostly stops being a question.* A reconciler
reads source and writes artifacts; where those live is a binding, external first and internal
later. The transition looked hard because a pipeline's stages name their targets.
## What this effort was right about
Its first question — *what is a deployed state, and how does the mesh know it is in one* — was
marked "everything follows from this", and everything did. Both records above are answers to it:
0058 makes the applier the reporter, and 0063 makes currency a comparison. The effort put the
load-bearing question first.
## What is NOT closed by this
[ADR 0010](../../02-DECISIONS/0010-delivery.md) names four costs
and one of them is a real risk rather than a trade: **a reconciler that cannot reach its target
retries forever, and without something that notices, the failure is silence** — which is the
fault this effort exists to catalogue, reintroduced in a new place. That belongs to observability
and it is not designed.
## The questions (all answered above)
| Question | Why it matters |
|---|---|
| What is a **deployed state**, and how does the mesh know it is in one? | Everything follows from this. If a stage reports transport, "deployed" is a claim nobody checked. A desired-state model with reconciliation gives a different answer from a job-completion model. |
| Does the coordinator dispatch **stages**, or converge nodes on a **declaration**? | The current model is a state machine over stages. The alternative is that a node is told what should be true and reports what is. The second makes drift visible; the first cannot see it. |
| How does a change **become** a pipeline, reliably? | Detection has failed for reasons unrelated to the change, silently. |
| What produces a **verdict**, and what is it a verdict about? | Ties to the lab ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)) and to a module carrying its own assertions. |
| How does delivery work **before self-hosting**, and across the transition? | From research 006: source and artifacts start external and are re-bound to internal providers. The coordinator has to be indifferent to which. |
| Does the **three-silo** split survive the artifact/part split? | [ADR 0010](../../02-DECISIONS/0010-delivery.md) is cardinality-driven, and research 006 renames the thing the cardinality is about. |
-95
View File
@@ -1,95 +0,0 @@
---
status: active
initiated: 2026-08-23
touches:
- 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
- 03-DESIGN/01-to-be/01-end-to-end-testing.md
- 02-DECISIONS/0016-the-lab.md
- 03-DESIGN/00-as-is/00-overview.md
became: []
---
# 009 — Getting from the mesh that exists to the mesh that is designed
## What is being investigated
How a running mesh becomes the one in
[research 006](../006-mesh-from-scratch/code-skeleton.md), without losing what it currently
carries.
The proposed shape: **build tiers 0, 1 and 2, then replace the current setup in one move.**
## Why big-bang is the right instinct here
Recorded because incremental is the reflex answer and it is wrong in this case.
- **The two models are structurally incompatible.** The tier rule, the host absorbing what are
now modules, the artifact/part split, provisioning generalised to the control plane's own
requirements — none of these can half-apply. Running both models at once means the old one's
assumptions keep constraining the new one, which is how a migration becomes permanent.
- **Nothing external depends on it.** No users outside the operator, no service level to hold.
- **The lab exists precisely for this** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
A big-bang that has been rehearsed end to end, repeatedly, on identical machines is not the
same risk as one performed for the first time on the real mesh. This is also why the lab is
phase 0 rather than a verification step later: the new mesh is *developed* inside it, so by
cutover the procedure has been run hundreds of times rather than rehearsed a few.
- **Incremental would carry the rot forward.** The as-is layer documents silent failure paths,
a dead test harness and unenforced rules. A gradual migration preserves them by definition.
## The distinction that lowers the risk
**Replace the control plane; do not move the workloads.**
The things that would hurt to lose — mail, media, source, databases, their data directories —
are not the mesh. They are what the mesh manages. They sit in container volumes on nodes, and
they do not need to move for the control plane above them to be replaced.
So the big-bang is: the old control plane stops managing these nodes, and the new one starts —
with the workload data untouched, in place, and re-declared rather than migrated.
That reframing turns "replace the mesh" into "replace the part with no persistent state of its
own", which is a materially smaller act than it first sounds.
## The tension this exposes
Taking over already-running workloads is **adoption**, and adoption was ruled out of scope —
recorded as a legacy path in
[`03-DESIGN/01-to-be/01-end-to-end-testing.md`](../../03-DESIGN/01-to-be/01-end-to-end-testing.md).
The migration appears to need exactly the capability the design declared it would not have.
The way out, to be tested: the new mesh does not adopt anything. It **declares** the workloads
from scratch and points them at data directories that already exist. Nothing inspects a running
machine to learn what is there; the declarations are written from the as-is layer, which is what
that layer is for. Data survives because it was never touched, not because it was adopted.
If that holds, adoption stays out of scope and the migration is ordinary declaration. If it does
not, adoption needs a one-time, explicitly unsupported tool, and that should be a decision rather
than a discovery.
## Sequencing
| Phase | What | Done when |
|---|---|---|
| **0** | **Build the lab's bootstrap scenario** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)) — virtual machines, a network, a way to place a binary, snapshot and reset. No forge, no coordinator, no pipeline. | A machine can be raised from nothing, reset, and raised again, repeatably. |
| A | Build tier 0, **inside the lab**. The host's interface first — it carries the skeleton's biggest unproven claim. | A bare machine becomes a managed node with no mesh present. |
| B | Build tier 1 and 2. The bootstrap scenario grows into the full one by addition — the same machines, with more placed inside them. | The lab raises a full mesh from nothing, repeatedly, from pinned external artifacts. |
| C | Enough of tier 3 to operate it. | The mesh can be driven without direct database access. |
| D | Declare the existing workloads against the new model. | The lab runs them, with copies of real data shapes. |
| E | **Rehearse the cutover in the lab** against a mesh built to resemble the real one. | Repeatable, and repeatably reversible. |
| F | Cut over. | The real nodes are managed by the new control plane. |
| G | Reach self-hosting — re-bind delivery from external providers to the mesh's own forge and registries. | The mesh builds and deploys itself. |
Phase G is deliberately last. Per research 006, self-hosting is a state the mesh **reaches**;
attempting the cutover and the self-hosting transition in the same move recreates exactly the
circularity the skeleton removes — and would mean a failed cutover could take away the means to
fix it.
## The questions
| Question | Why it matters |
|---|---|
| What state must **survive** the cutover, versus be re-created? | Workload data must. Provisioned credentials could be re-issued. The mesh's own inventory could be re-declared. Each answer changes the risk. |
| What is the **way back**? | A cutover with no rollback is not a plan. If workload data is untouched, reverting may be as small as re-pointing the old control plane at it — to be verified, not assumed. |
| How is the cutover **rehearsed** against something resembling the real mesh, without copying the real mesh into a repository? | The lab must be able to model the real topology's shape without carrying its identity. |
| Does anything have to keep running **during** the cutover? | Mail and source are the obvious candidates. If yes, "big-bang" is really "big-bang with exceptions", and the exceptions should be named now. |
| Is the forge inside or outside the cutover? | If the mesh's own forge goes down with the old control plane, the means of deploying a fix goes with it. This is the self-hosting circularity appearing as a migration risk. |
@@ -1,60 +0,0 @@
---
status: active
initiated: 2026-08-24
touches:
- 03-DESIGN/01-to-be/03-scenario-lifecycle.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
became: []
---
# 010 — What the lab's inner loop actually costs
## What is being investigated
[The lifecycle design](../../03-DESIGN/01-to-be/03-scenario-lifecycle.md) closes on an open
question that is measurable rather than arguable:
> **What a snapshot costs.** Whole-scenario snapshots of several machines are the operation the
> inner loop repeats most, so their cost sets the loop's speed. If restoring is slow, the loop
> is slow, and everything above is theory.
Measured on a workstation, 2026-08-24. Numbers in [`measurements.md`](measurements.md).
## Why it matters
[ADR 0016](../../02-DECISIONS/0016-the-lab.md) makes the
bootstrap scenario the inner development loop for tiers 0 and 1 — the argument being that
raising a node from nothing stops being the least-exercised path and becomes the most-exercised
one. **That argument is only true if raising and resetting are cheap.** A loop that costs
minutes is a loop people avoid, and the least-exercised path stays least-exercised.
## Status
Measured, and the finding is a blocker rather than a data point.
**A snapshot on the current host is a full copy of the machine's disk.** 1.6 GB and ten seconds
for one small virtual machine at best — and over two minutes when observed a second time. Cost
scales with the number of machines and the size of their disks, not with what changed.
**With copy-on-write it is 0.13 seconds and costs the delta.** Verified, not assumed. The
projected four-machine reset cycle falls from roughly ninety seconds to roughly fifteen, of
which almost all is a boot that cannot be avoided.
The cause is not virtual machines and not incus. It is that the host offers incus exactly one
storage driver, `dir`, which has no copy-on-write and therefore no cheap snapshot. The kernel
supports btrfs; the userspace tool that would let incus use it is simply not installed.
So the question *"is the lab's inner loop fast enough"* currently answers itself the wrong way,
for a reason that is one declared package away from being fixed — and declaring packages is
something the mesh already does.
## Open questions
| Question | Why it matters |
|---|---|
| ~~How much does a copy-on-write pool actually improve it?~~ **Measured: snapshot 9.9 s → 0.13 s, restore 10.4 s → 0.80 s, three snapshots sharing 1.36 GB rather than costing 4.8 GB.** The projected four-machine cycle falls from ~90 s to ~15 s. | Answered. The inner-loop argument holds *with* copy-on-write and did not without it. |
| How does the lab install its own prerequisites on a clean machine? | The lab needs a virtualisation daemon, copy-on-write tooling and a pool before it can do anything — and it cannot depend on the mesh for them, since it is where the mesh is built. |
| Why was the second snapshot more than twelve times slower than the first? | If snapshot cost is unpredictable rather than merely high, that is worse — a loop with a variable multi-minute step is one nobody trusts. |
| Does a scenario snapshot need the machines stopped? | Stateless snapshots of a running virtual machine capture the disk but not memory. Whether a mesh restored that way is coherent is not established. |
| What is the cost at scenario scale — four machines rather than one? | Only single-machine numbers were taken. If the operation is serial, four machines is four times the wait. |
@@ -1,141 +0,0 @@
---
effort: 010-lab-inner-loop-cost
updated: 2026-08-24
---
# Measurements
Taken 2026-08-24 on a workstation with hardware virtualisation available, an NVMe-backed ext4
root, and 300 GB free. One virtual machine, 1 GiB memory, 2 CPUs, from a cached distribution
image.
## The environment, before anything ran
| Fact | Value | Consequence |
|---|---|---|
| Hardware virtualisation | present | virtual machines run at native speed; the choice in [ADR 0016](../../02-DECISIONS/0016-the-lab.md) is not paying an emulation penalty |
| Storage drivers the daemon offers | **`dir` only** | no copy-on-write, therefore no cheap snapshot |
| Host filesystems | ext4 throughout | nothing copy-on-write to put a pool on |
| btrfs kernel module | **available** | the kernel can do it |
| `btrfs-progs` | **not installed** | which is the entire reason the driver is absent |
The last two rows are the finding. The daemon advertises only `dir` because the userspace tool
for anything better is missing — not because the host cannot do better.
## Raising a machine
| Step | Time |
|---|---|
| launch call returns | **3.4 s** |
| machine actually usable — a command executes on it | **14.3 s** |
The gap matters for the lifecycle design: `raise` returning is not the same as the scenario
being ready, so the verb has to wait for the second number, not report the first. Reporting
the first would be the mesh's own recurring failure — transport reported as effect.
## Snapshot and restore
| Operation | Time | Disk |
|---|---|---|
| snapshot, first | **9.9 s** | **+1.6 GB** |
| snapshot, second | **> 120 s — did not complete** | — |
| restore call returns | **10.4 s** | — |
| machine usable again | **20.1 s** total | — |
Instance on disk before snapshotting: 1.5 GB. Snapshot directory afterwards: 1.6 GB. **A `dir`
snapshot is a full copy** — the storage cost equals the instance, and nothing is shared.
Implied copy throughput on the first snapshot is roughly 160 MB/s, which is far below what the
underlying NVMe can do and is consistent with a real, durable copy rather than a metadata
operation.
**The second snapshot is the more troubling number.** It exceeded two minutes and was still
running when the observation was cut off; only the first snapshot exists. Whatever the cause —
page cache exhausted by the preceding restore, writeback contention — the practical
consequence is that snapshot cost here is **not merely high, it is unpredictable**.
## What this projects to
A four-machine scenario, taking the optimistic single-machine numbers and assuming the
operations are serial:
| | one machine | four machines |
|---|---|---|
| raise, to usable | 14 s | ~57 s |
| snapshot | 10 s, 1.6 GB | ~40 s, 6.4 GB |
| restore, to usable | 20 s | ~80 s |
A reset-and-rerun cycle is therefore **around a minute and a half at best**, and unbounded at
worst, before any of the mesh's own work begins.
## The judgement
**This is too slow for an inner loop**, and the reason is not the design.
[ADR 0016](../../02-DECISIONS/0016-the-lab.md) argues that
making the bootstrap path the inner development loop turns the least-exercised code in the
system into the most-exercised. That argument holds only while resetting is cheap. At a minute
and a half a cycle, with occasional multi-minute stalls, the loop is one a person works around
— and the path stays under-exercised for exactly the reason it always was.
Nothing about virtual machines causes this. Hardware virtualisation is present and the machines
boot in fourteen seconds. **The cost is entirely the storage driver**, and the driver is absent
because one userspace package is not installed on the host.
The mesh already has the mechanism for that: a module declares a package, and a hook makes it a
working capability — which is precisely what was just done for the virtualisation daemon
itself, and what [`04-ISSUES/007`](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md)
is about.
## The fix, measured
The comparison was subsequently run. One package — `btrfs-progs`, no dependencies — installed by
hand, the daemon restarted so it re-detected drivers, a copy-on-write pool created on a loop
file, and the identical image launched onto it.
| Operation | `dir` | copy-on-write | |
|---|---|---|---|
| snapshot | 9.9 s, then **> 120 s** | **0.13 s** | ~76× faster, and *consistent* |
| snapshot again | — | 0.12 s | |
| snapshot a third time | — | 0.13 s | |
| restore call | 10.4 s | **0.80 s** | ~13× faster |
| restore, to usable | 20.1 s | **10.5 s** | the remainder is boot, which is irreducible |
| three snapshots, storage | ~4.8 GB | **1.36 GB total, shared** | cost is the delta, not the disk |
**The fix is real, and larger than expected.** Snapshot goes from ten seconds to a tenth of a
second, and — more importantly — from *wildly variable* to *flat*. Three consecutive snapshots
took 0.13, 0.12 and 0.13 seconds. On `dir` the second snapshot never finished.
Storage stops scaling with the machine and starts scaling with what changed: three snapshots of
a 1.5 GB instance occupied 1.36 GB in total, because they share.
### What it projects to
A four-machine reset-and-rerun cycle, the operation the inner loop repeats most:
| | `dir` | copy-on-write |
|---|---|---|
| snapshot the scenario | ~40 s, 6.4 GB | **~0.5 s**, delta-sized |
| restore it | ~40 s + boot | **~3 s** + boot |
| **cycle** | **~90 s, unbounded at worst** | **~15 s, dominated by boot** |
At fifteen seconds, dominated by a boot that cannot be avoided, the inner loop is viable and
[ADR 0016](../../02-DECISIONS/0016-the-lab.md)'s argument holds.
At ninety it did not.
### One honest counter-observation
Launching onto the fresh copy-on-write pool was **slower** — 20.2 s to usable against 14.3 s —
because the image had to be unpacked into a pool that had never seen it. That cost is paid once
per pool, not per scenario, and it is dwarfed by what snapshotting saves. But it is a real
number and it went the other way.
### State this left behind
Recorded because hand-made state is exactly what the mesh's rules exist to prevent, and it must
be declared properly rather than left as an artefact of a measurement:
- `btrfs-progs` installed by hand. Its installation regenerated the boot initramfs, a side
effect worth knowing about.
- The daemon restarted once, to re-detect drivers.
- The test pool and instance were **removed**; the pool the lab actually needs does not exist.
@@ -1,203 +0,0 @@
---
status: graduated
initiated: 2026-08-25
became:
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0008-a-context-owns-its-store.md
- 03-DESIGN/01-to-be/06-the-controller.md
- 03-DESIGN/01-to-be/07-the-foundation.md
touches:
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 03-DESIGN/00-as-is/02-modules-and-manifests.md
- 03-DESIGN/00-as-is/10-module-catalogue.md
- 04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md
---
# 011 — The module graph
> **A third edge was found after this graduated.** This effort established *presence* and
> *instantiation*, and both are **runtime** edges — they answer *what does this need in order to
> run*. Delivery needs a different question answered — *what has to be rebuilt when this changes*
> — and that is a **build** edge, fixed inside an artifact rather than negotiated when it runs.
> Recorded by [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), which also
> notes what this effort's three entities turn out to be good for
> ([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)).
## What is being investigated
Whether the catalogue's missing structure is a **graph** — modules declaring what they need,
what they offer, and what they exclude — and what that replaces.
**[`worked-provider.md`](worked-provider.md) works one module through completely**, and breaks
the tidy version. A database is nine things, not one — and a small game asking the mesh for its
own database shows there are **two kinds of edge**: *presence*, where the thing must exist, and
*instantiation*, where a provider makes something for a consumer and hands back credentials.
Instantiation implies presence and not the reverse. The current system already had this split
and the design had collapsed it.
**[`features.md`](features.md) answers what happens to `feature`.** It is one word for four
things spanning three tiers — artifacts built once per version, resources applied to a machine,
actions run against something that is not this machine, and checks that are requirements in
disguise. Measured: every one of the twenty-one handlers implements all six stages, so `configs`
has a build stage with nothing to build and `npm` has a start stage with nothing to start. That
emptiness is the conflation, and it is why a stage that did nothing and a stage that failed look
alike. Nothing replaces it, because it was never one concept. **One property is worth keeping:
content is detected, relationships are declared.**
**[`cases.md`](cases.md) enumerates what a module can be** — twenty kinds of thing the mesh has
to install, run, own or know about — and extracts the axes a manifest must express. Two of those
axes appear in no current thinking: **how many instances** a thing may have, and **whether two
can coexist**.
**The design is in [`proposal.md`](proposal.md): one kind of edge.** A module provides names
and requires names, and that single relation absorbs requiring a module, requiring a resource,
and the interface-and-adapter idea. An abstract name is legitimate **only where providers are
genuinely substitutable** — `terminal` passes, `database` does not, because a consumer speaking
Postgres does not speak MongoDB. The adapter is what creates an interface; without one there is
a **tag**, which describes and does not bind.
A **node provides names too**, which makes capability checking stop being a separate mechanism
and makes the host's own capability report an input to resolution rather than something a
person reads.
[`analysis.md`](analysis.md) measured the current catalogue. Its value to the design is two
lessons rather than its machinery — a field that means *depends on* should say so, and
placement does not belong in a manifest — and the rest is recorded as as-is evidence.
**Measured, and the premise was wrong for the existing system: the graph is not missing there.**
[`analysis.md`](analysis.md) — 126 manifests, 103 edges, no cycles, nothing dangling, and a
resolver that topologically sorts them, already called by the tool loader, the installer and
the delivery coordinator. What the effort assumed would need building is a thing to call.
What survives is narrower: three declarations that do not exist (`excludes`, a required node
capability, an interface with adapters), and two defects worth fixing whatever else is
concluded — `provider:` is a dependency edge that is not read as one, which makes the closure
for a working mesh come out without a database; and the resolver continues past a cycle and
past a missing dependency, contrary to ADR 0001.
[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) proposes
grouping modules by domain. [Research 005](../005-domain-grouping/analysis.md) measured that
proposal and found its evidence holds in exactly one place — reachability — which
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) has since absorbed
into the host. The measured case for domain grouping has therefore been consumed by a decision
taken for unrelated reasons, and what remains is fifty modules that co-change with nothing.
That leaves the original complaint unanswered: the catalogue records **what was installed**
rather than **what anything is for**, and nothing in the system can see a relationship between
two modules. Grouping asserts relationships. A graph records them.
## Why now
A proposal to split modules into *provisioning services* and *applications* was worked through
and abandoned in favour of one concept with facets, for a reason worth keeping:
- The split cannot be filed consistently. A git forge is consumed as a service *and* operated
through a web interface. An analytics service grants tracking identity *and* is a dashboard
somebody reads. An identity provider grants authentication *and* has an admin console.
- The operator's correction is the sharper form: **what runs on the machine is a supervised
container, not something a user started.** That is a fact about *how a thing runs*, not about
what kind of thing it is — so it is a facet, not a taxonomy.
Filing decisions that follow from nothing are the disease research 005 measured. A second
taxonomy would reproduce it.
So [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) survives, and the question
becomes what a module must be able to **declare**.
## The shape being investigated
Five declarations, of which two exist today.
| Declaration | Today | Notes |
|---|---|---|
| **requires a resource** — a database, a bucket | yes | provisioning, [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) |
| **provides a resource** | yes | as above |
| **requires another module** | **no** | the dependency edge — the graph's substance |
| **excludes another module** | **no** | installing A makes B unavailable |
| **requires a node capability** | **no** | a graphical session, a container runtime, an architecture |
And one structural idea on top: **an interface module carries no implementation.** Adapters
provide it. An assistant interface with several model-provider adapters; a terminal interface
with several terminal adapters. A dependent names the interface and never an implementation.
**Prior art to measure against, not invent past.** This is a package manager's model, and the
platform's own package manager already has all of it: `depends` is the dependency edge,
`conflicts` is exclusion, and `provides` is the interface — several packages provide one
virtual name, and a dependent names the virtual one. That the design arrived at the same shape
independently is evidence for it. It is also a warning: dependency resolution, version
constraints, conflict handling and rollback are a long-solved and easily-botched problem, and
the effort should establish what to **delegate** rather than reimplement.
## Capabilities, and what may be installed
The operator's formulation: *system specs are capabilities, and capabilities unlock installable
modules — you cannot install a graphical application on a node with no display server.*
The question that follows is whether the mesh may install a capability. The effort's working
position, to be tested:
- **Intrinsic capabilities** — hardware, architecture, network position — are facts about a
machine. They are detected, never installed, and a module requiring one it does not have is
not unresolved but **impossible** on that node.
- **Provided capabilities** — a display server, a container runtime — are not a separate kind
of thing at all. They are modules that provide a capability, and requiring one is an ordinary
edge the graph resolves by installing it.
If that holds, "may the mesh install a capability" is not a policy question. It is dependency
resolution, and the only genuinely new thing is detection.
Which is where [issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md)
bears directly: *an installed package is not a capability*. A provided capability is not real
because a package is present — it is real when it is present, running and working, and the
difference is exactly the class of fault this repository keeps recording.
## What it touches beyond the catalogue
The operator's assessment is that the machinery around a module is wanted and its integration
is not: **scheduled tasks, hooks, migrations, configuration and environment settings are worth
keeping; seeds are not; and the current integration is wrong enough to need a major refactor.**
That is a claim to test rather than adopt. Research 005 already found supporting evidence from
a different direction — that the densest apparent coupling in the catalogue is manifest
boilerplate churn, cross-cutting changes to the machinery applied N times — which is what an
integration being wrong looks like from the outside.
## Open questions
Struck-through rows are answered, with where. The rest are live.
### Settled
| Question | Answer |
|---|---|
| ~~What does the graph **delete**?~~ | For the existing system: nothing, it is already there ([`analysis.md`](analysis.md)). For the design: the module/resource distinction, the interface as a kind of thing, capability checking as a separate mechanism, domain grouping, and — the first clear deletion — **grant kinds**, once a module may only be granted what it exclusively owns ([`worked-provider.md`](worked-provider.md)). |
| ~~Is an interface a module, or a name?~~ | A **name**, and only where providers are genuinely substitutable. The adapter is what creates one; without an adapter there is a **tag**, which describes and does not bind ([`proposal.md`](proposal.md)). |
| ~~Where do domain modules fit?~~ | They do not. There is core infrastructure — concrete modules named individually, not flavourable, nothing standing in front of them. |
| ~~What happens to domain grouping?~~ | Superseded. Folders assert relationships; edges record them. What grouping was for is a tag and a query. [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) is `proposed` and should be superseded rather than narrowed. |
| ~~Is there one kind of edge?~~ | **No — two.** *Presence*, where a thing must exist, and *instantiation*, where a provider makes something for a consumer and hands back credentials. Instantiation implies presence, not the reverse. |
| ~~When two modules provide one name, who chooses?~~ | Neither the consumer naming a node nor the consumer not caring. The consumer declares the **scope of its own need** — shared across its instances, or one each — the mesh binds, and the binding is written down and sticky. Where it is written follows the scope. |
| ~~Can several modules share one database?~~ | **No.** A module is granted only what it exclusively owns — no shared writes and no read role on another's store, because reading couples you to its layout just as firmly. |
| ~~What about a dashboard reading a dozen stores?~~ | **The rule is about contexts, not processes.** The mesh's own board reading the mesh's own store is the mesh showing its own data — not a boundary crossing. Everything inside a context reads its store freely; what is forbidden is a *different* context reading it. An earlier answer here was wrong. |
| ~~Can every registry consumer be served another way?~~ | **Largely dissolves.** Of eighteen direct consumers, the owner keeps its database, node appliers are already stopped by ADR 0005, and the bulk are **foreign tenants** — thirteen tables across three contexts — who need to move out rather than read differently. |
### Live
| Question | Why it is open |
|---|---|
| How many instances should a module have? | Not derivable and not global: one-per-mesh is right for the broker and wrong for a store a disconnected node needs. It is per-module and nothing in the schema says it. |
| Can two of something coexist? | `excludes` covers part of it. Two terminals are fine, two things wanting one port are not, two brokers might be either. |
| What does a provider hand back? | Credentials and an address for a store; a command for a terminal. Same relation, different shape crossing it. |
| Is provisioning one mechanism or two? | The mesh's own registry is provisioned **before there is a mesh**, so provisioning is part of the bootstrap and part of what the carried bundle expresses. At bootstrap the store is local; afterwards it is on another node. Same operation, both sides of a tier boundary. |
| Is a tool surface one relation with two audiences, or two? | 56 of 126 modules carry tools — more than carry a service — and what consumes them is an **agent**, not a module. |
| ~~Do the remaining cross-context reads want an interface or events?~~ | **Derived, not chosen.** Neither is SQL — that only ever runs against your own store. ADR 0004 makes disconnection ordinary, so anything that must work while disconnected cannot use a request and needs a local copy: a subscription. Anything where a stale answer is worse than none cannot use a subscription. |
| What does a consumer do about events it missed while disconnected? | Replay from a point, ask once for a full picture and resume, or rebuild. The question every projection has, and unanswered here. |
| What happens to a grant when its consumer is removed? | Dropping is data loss; keeping is a leak. ADR 0005's removal rule does not obviously carry, because the thing lives inside another module's state. |
| Is a declaration composed per node, from what that node reported? | Some configuration follows the hardware. Either the host fills a blank — deciding, against ADR 0005 — or the control plane composes from the node's inventory first. |
| Would `excludes` and capability requirements actually be used? | Zero manifests declare either, which is equally consistent with *nobody needs them* and *nobody can express them*. |
| What does an exclusion mean for something already installed? | Refuse the install, or surface the conflict and let it be decided. |
| Are tiers a view of the graph, or a constraint on it? | If a tier is a computed level the word is a convenience. If *a tier may depend only on tiers below it* is to be enforced, it is a constraint and must be stated as one. |
| Where does resolution happen — the mesh, or the platform's package manager? | The mesh must model mesh-level edges. Whether it also resolves operating-system packages decides whether a solver gets written. |
| How far may the control plane be split? | A single board over several contexts works because they are contexts *inside* one control plane with one interface. If a context becomes its own deployable with its own interface, the board is coupled to N of them and the composition has nowhere to live that tier 3 permits. A constraint on splitting, worth knowing before splitting. |
| What does the pipeline schedule, once features are gone? | It schedules features today. The four categories they split into have different lifecycles, so the unit of work differs for each and needs naming. |
| Should placement leave the catalogue? | A provision pins itself to a named node in the manifest. Placement is an inventory decision, and having it in the catalogue means a second node cannot provide the mesh's store without editing its consumer. |
@@ -1,138 +0,0 @@
# The graph is not missing
Measured against `origin/main` of the code repository, 2026-08-26. Every manifest, read through
git refs rather than a checkout.
The effort was opened to ask whether the catalogue's missing structure is a graph. It is not
missing. **It exists, it is healthy, and three separate parts of the system already use it.**
That is the finding, and it changes what is worth asking.
## Finding 1 — the graph is already declared, and it is clean
| | |
|---|---|
| manifests | 126 |
| declare a dependency on another module | 64 |
| declare a requirement on a provision | 11 |
| **edges** | **103** |
| declare neither | 62 — 49% |
| **cycles** | **0** |
| **dependencies declared but absent** | **0** |
| deepest chain | 5 |
Half the catalogue is unconnected, which matches
[research 005](../005-domain-grouping/analysis.md)'s finding that fifty modules co-change with
nothing. The connected half is well formed: no cycles, nothing dangling.
## Finding 2 — it is already resolved, and already used
The platform SDK carries a dependency resolver that topologically sorts modules, and it is
called from three places: the tool loader at startup, the installer when syncing modules onto a
node, and the delivery coordinator when expanding what a change affects.
It also already does something the effort assumed would need designing: **a requirement on
another module's provision is treated as an implicit edge to that module**, so a consumer does
not have to declare the same relationship twice.
So *ordering by the graph* — which
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says
the control plane will do — is not a thing to build. It is a thing to call.
## Finding 3 — the most important edges in the mesh are invisible
The one place the graph is wrong, and it is wrong about the substrate.
A module that needs a database declares it like this:
```yaml
provisions:
- name: mesh-db
provider: postgres # ← a dependency on the postgres module
node: <a named node> # ← and where it must run
```
`provider:` names a module. It is a dependency, declared, in the manifest — and it sits inside
`provisions:`, which is what a module *offers*. The resolver reads `dependencies:` and
`requires:`, so it never sees it.
| | |
|---|---|
| provider references that name a real module | 4 |
| **invisible to the resolver** | **3, across 2 modules** |
Three edges is nothing, and they are the mesh's own database, the mesh's own broker, and the
work engine's database. The most load-bearing relationships in the system are the ones the
graph cannot see.
**The consequence is measurable.** Computing what a working mesh needs, from the declared
graph:
```
registry → sdk → mesh → meshware 4 modules, 4 levels
```
No database. No broker. A closure that is arithmetically correct and obviously wrong, and wrong
for exactly one reason: a field that means *depends on* is not read as one.
This is [04-ISSUES/003](../../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)
again, in a new place — not a key nothing reads, but a key read as something other than what it
means.
## Finding 4 — the resolver continues past faults it should stop on
Two behaviours, both contrary to
[ADR 0010](../../02-DECISIONS/0010-delivery.md):
- **A cycle warns and falls back to input order.** A cycle means no correct order exists; the
resolver proceeds with an arbitrary one and logs a line.
- **A dependency that does not exist warns and continues.** The validation is documented as
*non-fatal, logged as warnings*.
Neither has fired in the current catalogue — there are no cycles and nothing dangling — which
is why nobody has noticed. They are latent, and they are in the component that
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) makes
responsible for the ordering a host will apply without question.
## Finding 5 — placement is decided in the catalogue
`node:` in a provision pins it to a named node, in the manifest. Two modules do this today,
and they are the substrate ones.
Which node runs what is an inventory and placement decision — tier 2 by the skeleton's own
test. Having it in a manifest means the catalogue decides placement, and a second node cannot
provide the mesh's database without editing the module that consumes it.
## What this means for the effort
**The opening question — "what does the graph delete?" — has an answer: nothing, because the
graph is already there.** The premise was wrong, and finding that out is the effort's first
result rather than a setback.
The questions that survive are narrower and answerable:
| Missing declaration | Manifests using it today |
|---|---|
| `excludes` — installing A makes B unavailable | **0** |
| a required node capability | **0** |
| an interface, with adapters providing it | **0** |
Those are what a graph would *add*. What it would delete is a different and smaller list, and
the honest version of it is: nothing yet.
**And two defects worth fixing regardless of what else this effort concludes:**
1. `provider:` is a dependency edge and is not read as one. Fixing it makes the closure correct
— which is what [research 012](../012-the-minimum-viable-node/00-overview.md) needs in order
to answer what a one-node mesh requires.
2. The resolver continues past a cycle and past a missing dependency. Both should refuse.
## What was not measured
- **Whether the missing declarations would be used.** Zero manifests declare exclusions or
capabilities, and that is equally consistent with *nobody needs them* and *nobody can express
them*. Nothing here separates those.
- **Whether the interface-and-adapter idea has a consumer.** It is a good shape, and it is
argued for rather than measured.
- **What the closure should be.** Finding 3 says the computed one is wrong. It does not say
what the right one is; that needs the fix first.
-128
View File
@@ -1,128 +0,0 @@
# What a module can be
Every kind of thing the mesh has to install, run, own or know about, before deciding what a
manifest says. Written to be argued with: a case here that turns out not to exist should be
struck, and one that is missing is a hole in whatever schema follows.
The hard cases are at the end, and they are the point.
## The ordinary cases
**1 — A supervised service.** A container the mesh runs and keeps running. Nobody starts it;
it is simply up. *A relational store, a message broker, an object store, a dashboard.*
**2 — A system package with configuration.** Not a container. Installed into the machine,
configured through files, run by the service manager. *A firewall, a resolver, an overlay.*
Note: [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says applying
these is the host's job — so what the module contributes is the *deciding*, not the doing.
**3 — An application a person launches.** Installed on a node, started by a human, running only
while they use it. *An editor, a chat client, a file manager, a terminal.*
**4 — A command-line tool.** Installed, on the path, run when invoked. No service, no window.
*A formatter, a query client, a backup utility.*
**5 — A library.** Never runs at all. Consumed at build time by other modules. *An SDK.*
**6 — A one-shot task.** Runs once, changes something, exits. *A schema migration, a data
import, a seed.*
**7 — A scheduled task.** Runs repeatedly on a timer, exits each time. *A backup, a prune, a
report.*
**8 — An adapter.** Exists to make several unlike things look alike behind one name. *A model
provider behind an assistant interface.*
**9 — A standalone application in its own repository.** Same shape as any of the above; the
difference is only where its source lives
([ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md)). Worth
listing because a schema that assumes a monorepo path would exclude it.
## The cases that break a naive schema
**10 — Something that is a service *and* an application.** A git forge is consumed by other
modules as a remote and a registry, *and* operated by a person through a web interface. A web
analytics service grants a tracking identity *and* is a dashboard somebody reads. Neither is a
service-or-application choice; both are true simultaneously.
**11 — Something that provides to others *and* consumes from others.** The store provides
databases and needs a filesystem. The forge provides a registry and needs a database. Provider
and consumer are not kinds of module; they are ends of edges.
**12 — Something the mesh installs that then becomes a node capability.** The container runtime
is installed *by* the mesh, and once it works, the node **provides** `container-runtime` to
everything else. So a module can change what its node provides. The node's provides-list is
therefore partly derived from what is installed on it, not only detected from what was already
there — and the two have to agree.
**13 — Something that must be adopted rather than installed.** The machine already has the
package manager, the container runtime, possibly the version control system, each with
configuration somebody chose. The module does not install it; it takes it over
([research 012](../012-the-minimum-viable-node/00-overview.md)).
**14 — Something that is a set, not a thing.** *Core infrastructure* is not installable — it is
a name for the concrete modules a working node needs. Whether that is a module whose only
content is `requires`, or a query over the graph, or a pinned list outside the catalogue, is
undecided and is one of the sharper questions here.
**15 — Something with one instance for the whole mesh.** There is one mesh database, not one per
node. Assigning it to two nodes is not redundancy, it is two meshes. Contrast with a terminal,
where per-node is the only sensible reading.
**16 — Something that may be installed several times over.** Two terminals coexist happily. Two
things wanting port 443 do not. Two message brokers might be fine or might be a split brain,
and nothing in *provides* and *requires* distinguishes those.
**17 — Something that is not software at all.** A firewall policy. A DNS record. A certificate.
It owns no binary, runs nothing, and is entirely *desired state* — which is the one case that
fits the host's declaration model exactly and fits an installable-package model not at all.
**18 — An agent.** The mesh's own premise is that agents are participants. An agent has an
identity, a licence, a node it runs on, and work it does. Whether that is a module, a record in
the control plane, or something else is not obvious, and getting it wrong shapes everything
about how agents are assigned.
**19 — The host itself.** Tier 0 installs everything else and is installed by hand. It is not a
module, and a schema that cannot say so has a bootstrap problem hiding in it.
**20 — Something the mesh depends on and does not control.** A domain registrar, an upstream
resolver, a certificate authority, an electricity supply. Almost certainly not modules — but
the mesh's health depends on them, and they are the reason a node can be perfectly configured
and still not work.
## What varies across the cases
The list above matters less than this. These are the axes a manifest has to express, and each
one is a question the schema must answer or deliberately refuse.
| Axis | Range | Sharpest case |
|---|---|---|
| **Does it run?** | supervised · launched by a person · once · on a timer · never | 5, 6, 7, 17 |
| **Who starts it?** | the mesh · a human · nothing | 1 vs 3 |
| **Where does it come from?** | container image · system package · our source · already on the machine | 12, 13 |
| **Does it provide to other modules?** | a resource · an abstract name · nothing | 8, 11 |
| **Does it hold state?** | yes, and it matters where · no | 1 vs 3 |
| **How many instances?** | one per mesh · one per node · many per node | 15, 16 |
| **Can two coexist?** | yes · no · only with different settings | 16 |
| **Is it installed or adopted?** | installed · adopted · either, depending on the machine | 13 |
| **Does installing it change what the node provides?** | yes · no | 12 |
**Two of these are not in any current thinking**, and both come from the hard cases:
*how many instances* (15) and *can two coexist* (16). `excludes` covers part of the second and
nothing covers the first.
## Questions the cases raise
- **Is "runs" a property or a kind?** The axes suggest a property — one schema, with a field
saying how it runs, `never` included. The alternative is several kinds of module with
different schemas, which is the taxonomy [research 011](00-overview.md) already rejected once
for services and applications.
- **Is an agent a module?** (18) If yes, the schema carries identity and licensing. If no, the
mesh has two catalogues.
- **Is core infrastructure a module?** (14) A module whose only content is `requires` is either
elegant or a category pretending to be a thing — the same trap `database` was.
- **What names one-per-mesh?** (15) Nothing in provides, requires or excludes says it, and
getting it wrong means two of something that must be one.
- **Where does a policy live?** (17) Pure desired state fits the host's declaration exactly.
Whether it is a module at all, or something the control plane derives and no catalogue entry
exists for, is open.
@@ -1,131 +0,0 @@
# What a feature is, and what it splits into
Measured against `origin/main` of the code repository, 2026-08-26.
The operator wants the feature concept gone.
[Research 006](../006-mesh-from-scratch/00-overview.md) left it open — *"does `feature`
survive? The skeleton splits it in two and argues the conflation is what makes the delivery
pipeline hard to reason about."*
This is what it actually is, and what it turns into.
## What it is today
A **feature** is a kind of content a module can carry, detected from what its directory
contains rather than declared. Twenty-one of them, each with a handler that owns its whole
lifecycle:
```
configs dist-assets events hooks migrations migration-artifact npm npm-install
prerequisite-env prerequisite-packages prerequisite-provision provision-migrations
provision-seeds seeds service systemd tools verifiers verify vhost detect
```
Each moves through **six stages**: build, publish, install, configure, start, verify.
## Finding — every handler implements every stage
The structural evidence, and it is not a style problem.
| handler | stages it implements |
|---|---|
| `configs` | build · configure · install · start · verify |
| `service` | build · configure · install · start · verify |
| `systemd` | build · configure · install · start · verify |
| `tools` | build · configure · install · start · verify |
| `vhost` | build · configure · install · start · verify |
| `migrations` | build · configure · install · start · verify |
| `npm` | build · publish · install · configure · start |
`configs` writes files onto a node. It has nothing to build, and it has a build stage.
`npm` publishes a package to a registry. It has nothing to start, and it has a start stage.
**One interface spans build-time and apply-time, so every kind of content must implement both
halves and most of them do nothing in one.** That is the conflation, and the emptiness is what
makes the pipeline hard to reason about: a stage that does nothing and a stage that failed to
do anything look identical from outside.
## What it splits into
The twenty-one are not one kind of thing. They are four, and they belong to different tiers.
**Artifacts — built once per version, then published.** `npm`, `dist-assets`,
`migration-artifact`. Nothing about a node is involved; the output is a thing that exists in a
registry. **Tier 2, delivery.**
**Resources — desired state on a machine.** `configs`, `service`, `systemd`, `vhost`, `tools`.
Applied, converged, idempotent — which is exactly what
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
already describes and what the host already does. **Tier 0.**
**Actions — run once, against something that is not this machine.** `migrations`, `seeds`,
`provision-migrations`, `provision-seeds`, `hooks`. A migration runs against a database, and the
database may be on another node entirely. Neither an artifact nor node state, which is why they
sit awkwardly in a scheme built for both. **Tier 2, and the operator wants seeds gone.**
**Checks — assertions, not changes.** `prerequisite-env`, `prerequisite-packages`,
`prerequisite-provision`, `verify`, `verifiers`, `detect`. The prerequisites are **requirements
in disguise** — a module saying what must be true before it can be installed, which is precisely
what an edge in the graph says. The verifiers are the read-back the host already performs.
## So the answer is: it splits, and the split is a tier boundary
**`feature` is one word for four things spanning three tiers.** That is why every handler
implements every stage, why half of them are empty, and why the pipeline is hard to reason
about.
Nothing replaces it, because it was never one concept:
| Was a feature | Becomes | Whose |
|---|---|---|
| npm, dist-assets, migration-artifact | an **artifact** | delivery |
| configs, service, systemd, vhost, tools | a **resource** in a declaration | the host |
| migrations, hooks | an **action** against something else | delivery |
| seeds | *nothing* — the operator wants them gone | — |
| prerequisite-* | an **edge** in the graph | the catalogue |
| verify, verifiers | the host's **read-back** | the host |
## Tools are a facet, and their consumer is not a module
The most common content in the catalogue: 56 of 126 modules carry a tool surface, more than
carry a service.
It survives the split, and it does not fit either half cleanly. A tool is not an artifact and
not node state — it is a **contract the mesh publishes on a module's behalf**, and what consumes
it is an **agent**, not another module.
That is a second audience, and the design has only described one. A module `provides` things
other modules require; a module also `provides` things agents call. Same word, different
consumer, different lifecycle — a tool appears when the module is assigned somewhere and
disappears when it is not, and nothing in the graph edges says so.
Whether that is one relation with two audiences or two relations is undecided, and it is the
kind of question that is cheap now and expensive later.
## What is lost, and should not be
**Detection.** Features are detected from the directory rather than declared, and the reason is
good: *a declared list and the directory it describes drift, and the directory is the one that
is true.* That property is worth keeping whatever the concept is called — a module that says it
has migrations and has none, or has them and does not say so, is a fault nobody sees until it
matters.
Under the split, detection still applies: what a module **contains** is read from what is there.
What it **requires**, **provides** and **excludes** is declared, because none of that is visible
in a directory.
That line is worth stating precisely, because it is the one the current design got right:
**content is detected, relationships are declared.**
## Open
- **Is an action a resource?** A migration is not node state and not an artifact. It could be a
resource type the host applies with the target being a database rather than a machine — which
would collapse the third category into the second, at the cost of the host reaching something
that is not the machine it is on. That cost looks too high, but it has not been argued.
- **Where do hooks go?** They are arbitrary code a module runs at a stage — the escape hatch. A
design with no escape hatch is either very good or has not met reality yet, and this one has
not.
- **What does the pipeline schedule, once features are gone?** It currently schedules features.
If the four categories have different lifecycles, the unit of work is different for each, and
what the coordinator orchestrates needs naming.
@@ -1,167 +0,0 @@
# One kind of edge
> **Superseded in part by [`worked-provider.md`](worked-provider.md).** Working postgres through
> completely shows there are **two** kinds of edge, not one: *presence* — the thing exists and is
> reachable, nothing created — and *instantiation* — the provider is asked to make something for
> this consumer and hands back credentials. Instantiation implies presence; presence does not
> imply instantiation. Everything else below stands; the claim in the title does not.
A design, not an account of what exists. [`analysis.md`](analysis.md) measured the current
catalogue and its value here is two lessons rather than its machinery: a field that means
*depends on* should say so, and placement does not belong in a manifest.
## The idea
**A module provides names. A module requires names. That is the only edge.**
Everything the effort listed as separate declarations turns out to be one relation with
different kinds of name on either end.
```
postgres provides postgres
kitty provides kitty, terminal
xterm provides xterm, terminal
anthropic provides anthropic, ai-assistant
meshboard requires postgres, lavinmq
vscode requires terminal, display-server
```
A name is either **concrete** — a module's own name, so `requires: postgres` means that module
and no other — or **abstract**, so `requires: terminal` means whatever provides it.
That single relation absorbs three things the effort had listed separately:
| Was going to be | Is |
|---|---|
| requires another module | requires a concrete name |
| requires a resource | requires the module that provides it, by name |
| an interface, with adapters providing it | an abstract name, legitimate only where an adapter makes providers substitutable |
The interface stops being a kind of module. It is a name with more than one provider *and a
contract they all satisfy*, and nothing has to declare that it is one.
### An abstract name is only legitimate when providers are actually substitutable
The test, and it is a strict one: **can a consumer be switched from one provider to another
without changing?**
`terminal` passes. Anything that runs a command in a terminal works, and a consumer never
learns which one it got.
**`database` fails, and it is the example worth keeping.** A module connecting to Postgres does
not connect to MongoDB, or to SQL Server, or to MariaDB. Different wire protocol, different
dialect, different driver. A consumer that declared `requires: database` and was handed any of
them would break — so the name promises something no provider can deliver, and the resolver
would satisfy a requirement that is not satisfied.
That failure has a shape this repository already knows: something declared, accepted, and not
true. It is worse here than usual because the graph would report success.
**The adapter is what creates an interface.** `ai-assistant` is a legitimate abstract name
exactly when adapters exist to normalise the providers behind it. Without an adapter there is
no interface — there is a category, and a category is not an edge.
So `postgres` is required by name, and a module that could genuinely work with several stores
requires whichever it actually speaks to.
### Categories are catalogue metadata, not structure
*Database* is still a useful word — for finding things, for a person browsing what the mesh can
host, for grouping in an interface. It is a **tag**.
Tags describe. Edges bind. Keeping them apart is what stops the catalogue acquiring a second
kind of relationship that looks like a dependency and is not — which is what a folder named
after a domain already was.
## The node provides names too
The move that makes capabilities stop being a separate system.
A node's profile is a set of provided names. `display-server`. `container-runtime`.
`amd64`. A module requiring `display-server` is satisfied by **the node**, exactly as a module
requiring `postgres` is satisfied by another module.
So there is one resolution rather than two: *is this name provided by anything available here?*
A graphical application cannot be installed on a node with no display server for the same
reason, and through the same code, that it cannot be installed without its libraries.
And it means the host's capability detection — which already reports what a machine can be
asked to do, with a reason for each verdict — **is the node's provides-list**. It was built to
be read by a person; it turns out to be an input to resolution.
## What a module declares
```yaml
name: meshboard
provides: [meshboard]
requires: [postgres, lavinmq, container-runtime] # by name: it speaks their protocols
excludes: []
tags: [observability] # for finding it, never for resolving it
runs: # what applying it means
- container: ...
constraints: # what must be true of a node, never which node
- architecture: amd64
```
`postgres` and `lavinmq` are concrete because this module speaks their wire protocols and would
break against anything else. `container-runtime` is abstract and satisfied by the **node**.
**`excludes`** is the one genuinely new relation: naming something that cannot coexist with
this. It is not derivable from requires and provides, and without it two modules that both
provide `terminal` — or two that both want port 443 — look independent right up until
installing the second breaks the first.
**`constraints` are not placement.** They say what must be true of a node, never which node.
Which node runs what is an inventory decision, and the measurement found the current catalogue
deciding it in the manifest — a module pinning its database to a named node, so that a second
node cannot provide it without editing the module that consumes it. That is the mistake this
separation exists to avoid.
## What it deletes
The question the effort opened with, answered for the design rather than for what exists.
- **The distinction between requiring a module and requiring a resource.** One relation.
- **The interface as a kind of thing.** A name with several providers.
- **Capability checking as a separate mechanism.** The node is a provider.
- **The domain module** — a `networking` module that exists to gather a firewall, a resolver and
a proxy under one name. It came from an older shape and does not fit: there is no such thing
to install. There is **core infrastructure**, which is a set of concrete modules named
individually — a firewall, a store, a resolver — with no flavour and no grouping module
standing in front of them.
- **Domain grouping as structure** ([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)).
Folders assert relationships; edges record them. What grouping was for — finding things,
seeing what belongs together — is a **tag** and a *query* over the graph, neither of which
anybody has to keep true by hand.
- **Tiers as a separate concept**, possibly. A tier is a level in the graph, and levels are
computed. Whether the coarse boundary is still worth naming is below.
## What it does not delete, and should not
**A resolver still has to exist**, and it will need version constraints, conflict handling and
a story for when two modules provide one name and nothing says which to use. That is the
long-solved and easily-botched part, and the design should say what it **delegates** to the
platform's package manager rather than reimplementing.
## Open
- **When two modules provide one abstract name, who chooses?** A node with both `kitty` and
`xterm` satisfies `terminal` twice. Whether the choice is a node setting, a mesh setting, or
an explicit pin is the substance of the interface idea, and this proposal does not settle it.
It is a smaller question than it was: the strict substitutability test means the consumer
genuinely does not care which it gets, so the choice is about preference rather than
correctness.
- **Are tiers a view of the graph, or a boundary that survives it?** If tiers are levels, the
concept is derived and the word is a convenience. If the tier rule — *a tier may depend only
on tiers below it* — is meant to be enforceable, it is a constraint on the graph rather than
a description of it, and it has to be stated as one.
- **What does a provider hand back?** A consumer requiring `postgres` needs credentials and an
address; one requiring `terminal` needs a command. The requirement is satisfied by the same
relation in both cases, and what flows across it is not the same shape. Whether that lives in
the name, beside it, or in what the provider returns is undecided.
- **What does a node provide that is not a capability?** Its architecture is a provided name
under this scheme, and so is its operating system. That may be elegant or may be one
abstraction too far; nothing here tests it.
@@ -1,491 +0,0 @@
# A provider, all the way through
One module, worked out completely, because it is the case that breaks the tidy version. It
looks like *a container that runs a database* and it is at least nine things.
Postgres is the example. **The shape is not specific to it** — see the end.
## What it carries
**1 — A supervised container.** An image, a version, a data volume, and configuration. Easy,
and the only part the phrase "a docker service" describes.
**2 — Persistent state, and where it lives matters.** The volume is the database. Moving this
module between nodes is not rescheduling; it is a migration. Almost nothing else in the
catalogue has this property, and nothing in `provides` / `requires` expresses it.
**3 — Configuration that is partly the machine's.** Tuning follows the hardware — memory,
storage. A declaration generated centrally cannot know those, and
[research 012](../012-the-minimum-viable-node/00-overview.md) says the machine's own values win
on conflict. So some of this module's configuration is *derived from the node it lands on*.
**4 — A tool surface.** It exposes capabilities agents can call — query, list, provision. That
is not a resource on a machine and not an artifact; it is a contract the mesh publishes on the
module's behalf.
**5 — A provisioner.** The part that matters, and the one below.
**6 — Its own bookkeeping.** The provisioner must remember what it granted to whom, or it
cannot revoke, rotate or clean up. So a module that provides state to others *also* holds state
about its providing — and that state is not the database's data.
**7 — An exposure decision, per node it runs on.** Reachable from the machine only, from the
local network, or publicly. That is a property of *this assignment*, not of the module: the
same module on two nodes may answer differently.
**8 — Credentials it generates.** Per consumer, and they have to reach the consumer. Which
means a provisioning edge carries a payload, and the payload is a secret.
**9 — Health that is not "the container is up".** A container running and a database accepting
connections are different facts, and the second is the one anything cares about. This is the
host's read-back rule, at a distance.
## The provisioner is a second kind of edge
The tidy version of this effort said: *a module provides names, a module requires names, that is
the only edge.* A small game wanting to store data shows it is not.
```
my-cool-game requires postgres # I speak its protocol, it must exist
my-cool-game requires a database FROM postgres, called my-cool-game
```
The first is **presence**: the thing exists and is reachable. Nothing is created; nothing flows
back. `vscode requires terminal` is this, and so is `requires container-runtime`.
The second is **instantiation**: the provider is asked to make something *for this consumer*,
and hands back what the consumer needs to use it. A database, a user, a password, an address.
They differ in every way that matters:
| | presence | instantiation |
|---|---|---|
| creates something | no | yes, one per consumer |
| carries a payload back | no | credentials, an address |
| can be revoked | — | yes, and must be when the consumer goes |
| provider holds state about it | no | yes — who was granted what |
| satisfied by | anything providing the name | that provider, specifically |
**This is the mesh's actual power**, in the operator's words: a small game declares it wants a
database and the mesh makes one. Nobody creates a user by hand, nobody pastes a connection
string. That is worth being precise about rather than folding into a single relation because
one relation is prettier.
## What that costs the design
**The proposal's "one kind of edge" is wrong**, and the current system already knew: it has
`dependencies` for presence and `requires: provision:` for instantiation, with the resolver
deriving a presence edge from every instantiation edge. [`analysis.md`](analysis.md) recorded
that derivation as a convenience. It is not — it is the correct relationship between two
genuinely different relations.
So: **two kinds of edge, one graph.** Instantiation implies presence. Presence does not imply
instantiation.
## Which provider, when two nodes run one
Asked directly, because two nodes can each run a relational store and a consumer has to be
served by one of them. Neither obvious answer is right.
**Not "the consumer names the node."** That is placement in the consumer's manifest — a small
game edited because a database moved, which is the fault
[`proposal.md`](proposal.md) separates constraints from placement to avoid.
**Not "the consumer does not care" either.** For presence it genuinely does not: a terminal is a
terminal. For instantiation it cares permanently, because the data lands in exactly one store
and the wrong choice is discovered long afterwards.
**What the consumer does know is the scope of its own need.** Not which node — how many of the
thing it wants, relative to itself:
| Scope | Means | Example |
|---|---|---|
| **shared** | one instance serves every instance of this consumer | the mesh's own registry: every node reads the same rows |
| **per instance** | each instance of this consumer gets its own | a local cache, a per-node queue |
That is a property of the consumer, expressible without naming anything. And it is the thing
that actually decides: a shared need cannot be satisfied by a provider each node runs
separately, and a per-instance need should not be satisfied by a shared one.
**Then the mesh binds, and the binding is written down.** Not recomputed: a resolver that
re-derives which store serves a consumer will one day derive a different answer and relocate a
database, so the binding is made once and changed only deliberately.
**Where it is written down follows the scope.** A shared grant belongs to the *module* and every
assignment of it references the same one — which is the answer for one module installed on two
nodes wanting one database between them. A per-instance grant belongs to the *assignment*. Same
relation, two homes, and which home is not a detail: it is what makes two instances share
something or not.
The pieces that follow, none of them settled here:
- **When several providers satisfy the scope**, something chooses — most plausibly locality,
preferring a provider on the same node. That is a default, and it must be overridable, because
the reason to override it is exactly the reason nobody anticipated it.
- **A binding is a thing that can be wrong.** Once recorded it can be inspected, and a consumer
bound to a store on a node that no longer exists is a question somebody can be asked rather
than a failure at connect time.
- **Moving a binding moves data.** Whatever the mechanism, changing it is a migration and not a
configuration change, and a design that lets it look like the latter will lose something.
## Provisioning is early, not late
An assumption worth killing: that provisioning is something the control plane does for
consumers once a mesh is running.
**The mesh's own registry is a provisioned database.** So is its virtual host on the broker.
Neither exists until something creates them, and nothing in the mesh works until they do. The
order is:
```
1 the store runs from the bundle the host carries
2 a database is created in it a provisioning step
3 the mesh's own schema is applied a migration, against that database
4 the control plane starts and only now is there a mesh
5 everything else is provisioned the ordinary path
```
Steps 2 and 3 happen **before there is a mesh to do them**. So provisioning is not a
control-plane service that consumers use; it is part of the bootstrap, and part of what the
carried bundle has to be able to express.
**Which strains what a declaration is.** [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
has the host applying *declared state on this machine*. A database inside a running store is not
a file or a unit — and at bootstrap it is, at least, local: the store is on the same machine as
the host applying the bundle.
Later it is not. A consumer on one node provisioned from a store on another is the ordinary
case, and reaching it is not the host's job.
**Resolved as two mechanisms, which is the answer rather than a compromise**
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)). The host
runs bootstrap actions locally from the bundle; the control plane provisions across the mesh
afterwards. Different actors, different scopes, different trust paths — so there is no single
operation with a tier boundary running through it.
## Several modules, one database — and the case for refusing
Asked, then reconsidered by the operator: *maybe we should not allow it.*
The permissive version was a per-consumer **schema** inside a shared database — its own
namespace, its own migrations, revocable by dropping the schema, with a cross-context join
possible but deliberate.
**The stricter version is better, and it goes further than schemas.**
> **A module is only ever granted a resource it exclusively owns.**
No shared writes. And **no read-only role on another module's database either** — reading
another context's tables couples you to its layout exactly as firmly as writing them does, and
the coupling is harder to see because nothing breaks until the owner changes a column.
That is what [`how-we-build`](../../00-META/how-we-build.md) §4 already says: *contexts integrate
through the record, never through a shared schema.* The permissive version kept the letter of it
and left the temptation in place, and the path of least resistance wins eventually. A boundary
that is merely inconvenient to cross is a boundary that gets crossed.
### What it costs
**Cross-module reporting.** Anything wanting to know what several modules hold can no longer
join across them. It consumes their events, or calls their interface, and neither is as
immediate as a query.
That cost is the point rather than a regrettable side effect — it is §4's whole argument, and
the mesh already has both mechanisms: an event stream every context publishes to, and a tool
surface every module exposes. What gets harder is the thing that was making work belonging to
one context keep having to be implemented in another.
**One more connection per consumer.** A dozen modules means a dozen databases rather than a
dozen schemas in one. For a relational store this is unremarkable; it is worth stating only so
nobody discovers it as a surprise.
### What it deletes
The effort has been looking for what the design *removes* rather than adds, and this is the
first clear instance:
- **Grant kinds.** There is one — an exclusive resource. No schema grants, no read roles, no
scoping rules for who may see what inside a shared thing.
- **The question of who owns which table**, and with it the guessing at revocation time.
- **Cross-module migration ordering.** Two modules migrating one database need their migrations
ordered against each other. Exclusive ownership means a module's migrations are ordered only
against itself.
- **A whole class of permission modelling** that a shared store would otherwise need.
### The rule is about contexts, not processes
An earlier version of this file argued that a dashboard reading a dozen stores was caught by the
rule, because a dashboard is a surface and surfaces speak to an interface. **That was wrong, and
it drew the line in the wrong place.**
The mesh's own board showing nodes, modules and deployments is not a separate context reaching
across a boundary. It is the mesh showing its own data. Reading that store is not a violation
however it is done, and requiring it to go through an interface to reach facts its own context
owns would be ceremony.
**The rule is: a context is granted what it exclusively owns.** Everything inside that context —
its service, its surface, its tools — reads it freely. What is forbidden is a *different*
context reading it.
Which is exactly what the count below shows: the problem was never surfaces. It was three other
contexts keeping their tables in the mesh's database.
**And one surface over several contexts is normal.** The board visualises the mesh, the work
engine, the knowledge base and more, and the alternative — a separate web application per
context — is worse for everyone who uses it. That is not a compromise with the rule; composing
several sources into one view is what a surface *is*.
What it changes is only where it reads from: each context's **interface**, not each context's
**store**. Most of that already exists — 56 of 126 modules carry a tool surface, more than carry
a service.
**And the unified board is what keeps those interfaces honest.** If a view cannot be built from
a context's interface, that interface is inadequate — discovered in the one place where it is
cheap to notice, rather than the first time something else needs the same data and quietly
reaches for the store instead.
**But "the board calls each context's interface" is not quite it either**, and the objection is
right: if every context runs its own service with its own interface, the board is coupled to N
of them instead of N schemas, something has to compose them, and composition is logic — which
tier 3 says a surface does not hold. That moves the problem up a layer rather than solving it.
**The skeleton already answers this and the argument above talked past it.** `work` and
`knowledge` are not separate services; they are **contexts inside the control plane**, alongside
the record, inventory, delivery and the rest — and `api` is listed there as *the one interface
every surface speaks to*.
So the board speaks to **one** interface. Behind it the contexts stay separate: separate stores,
integrating through the record. But they are one tier, one repository, one deployable, and
coupling *within* a tier is not what the tier rule forbids.
Which resolves the objection rather than deflecting it: the problem does move up a layer, and
the layer it moves to already exists and has this as its job.
**The caveat is load-bearing.** This holds only while the contexts are not separate deployables.
The moment one becomes its own service with its own interface, the board is back to N clients,
something must compose them, and the composition has nowhere to live that tier 3 permits. That
is a real constraint on how far the control plane may be split, and it is worth knowing now
rather than discovering it by splitting.
If composing even one interface turns out too slow, the answer is a projection the board owns
and keeps current from events — not access to somebody else's tables.
### Checked against the real consumers
The rule's survival turned on whether every reader of the mesh's own registry could be served
some other way. **Eighteen consumers open a direct connection to it.** Four groups, and only one
of them is work.
| Group | What happens under the rule |
|---|---|
| **The owner and its machinery** — the mesh module, the SDK, the environment and configuration synchronisers, secrets | Nothing. It owns the database. |
| **Node appliers** — the overlay, the shell daemon, the resolver | **Already resolved.** [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) stops the host querying the mesh database, decided for tier reasons with nothing to do with this. |
| **Foreign tenants** — the work engine (10 tables), the knowledge base (2), pipeline logs (1) | They need **their own database**. They are not reading the registry; they are storing their own data in it. |
| **Genuine cross-context reads** — the work engine reads `nodes`; two others read a handful | The only ones needing an interface or events. |
**Thirteen foreign tables live in the mesh's registry database**, belonging to three separate
contexts. That is [`how-we-build`](../../00-META/how-we-build.md) §4's shared schema, counted.
**So the question was the wrong shape.** The bulk of the problem is not readers needing a new
route to data — it is **tenants needing to move out**. Tasks, agents and teams have nothing to do
with nodes and modules; they are co-located by history. Give that context its own database and
its dependency on the registry shrinks to a single table.
What remains is a handful of genuine cross-context reads, small enough to enumerate rather than
estimate. **The rule holds.**
### Request or subscription, and what decides
The remaining cross-context reads need one or the other. **Neither is SQL** — under exclusive
ownership a module runs SQL against its own database and nothing else, whatever transport a
query might travel over. Both options are the mesh's own channel, and both ride the broker
([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)), so the transport is
not the distinction.
**The distinction is where the answer lives when you need it.**
| | request | subscription |
|---|---|---|
| you ask | at the moment you need to know | never — you are told |
| the answer lives | on the other side | in your own store |
| freshness | always current | as current as the last event you received |
| when the other side is down | you cannot answer | you answer from your copy |
| what you must handle | a round trip that can fail | events you missed while you were down |
**What decides is not taste.** [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)
makes disconnection an ordinary situation rather than an exception. So:
> **Anything that must keep working while disconnected cannot use a request** — there is nobody
> to ask. It needs a local copy, which means a subscription.
And the converse: anything that must be *correct at the instant of asking*, where a stale answer
is worse than no answer, cannot use a subscription. A display can lag. A decision about whether
a grant is still valid cannot.
That turns an apparently open question into a derived one. What remains genuinely open is
narrower: **what a consumer does about the events it missed** while it was disconnected — replay
from a point, ask once for a full picture and resume, or rebuild from scratch. That is the same
question every projection has, and nothing in the record answers it yet.
## A migration belongs to the consumer and runs on the provider
A game defines migrations. They run against the database the store granted **it**. So the
migration is:
- **owned** by the consumer — it is that module's schema, versioned with that module;
- **hosted** by the provider — it runs inside something the consumer does not control;
- **ordered** after the provisioning edge — there is nothing to migrate until the grant exists;
- **scoped** to the grant — the consumer's migrations touch its database and no other.
Ownership crosses the edge, which nothing in *provides* and *requires* expresses. And it is the
*action* category from [`features.md`](features.md) made concrete: not an artifact, not node
state, and not something the host can apply, because the thing it changes is not the machine.
It also gives a consumer's own install an internal order — **provisioned, then migrated, then
started** — that depends on an edge rather than on the module's contents.
## What still has no answer
**How many instances of postgres should exist?** One per mesh is wrong — a node that must work
while disconnected cannot depend on a database elsewhere. One per node is wrong — the mesh's own
registry is one thing, not one per node. So the answer is per-module, and nothing in the schema
says it. This is [`cases.md`](cases.md) axis *how many instances*, and postgres is the case that
proves it cannot be a global rule.
**What happens to a grant when the consumer is removed?** The game is uninstalled. Its database
still exists, holding its data. Dropping it silently is data loss; keeping it forever is a leak.
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says
the host removes what it applied and no longer declares — but this is not on the host, it is
inside another module's state, and the same reasoning does not obviously carry.
**Where does node-derived configuration come from?** (3) The control plane composes a
declaration, and cannot know this machine's memory. Either the host fills in a blank the
declaration leaves — which makes the host decide something, against
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) — or the control
plane reads the node's inventory first and composes with it. The second is consistent and means
a declaration is composed *per node from what the node reported*, which is a stronger claim than
anything recorded so far.
## Providing is not a substrate thing
The four pinned services are the obvious providers, and they are not a category.
| Service | What a consumer asks it for |
|---|---|
| a relational store | a database, a user, credentials |
| another relational store, different vendor | a database — **and not the same one** |
| a message broker | a virtual host, a user, permissions |
| an object store | a bucket and keys |
| an image registry | a repository |
| an identity provider | a client, a realm, a secret |
| an analytics service | a site, and a tracking identity |
| a low-code data platform | a base, and a token |
| an application platform | a project, which is several of the above at once |
| a mail server | a mailbox, an alias, credentials |
**Any hosted service can be a factory.** Providing is a facet a module may have, not a kind of
module it is — which is the same conclusion the effort reached about services and applications,
arriving from the other direction.
That kills the last reason to keep *provider* as a category. A module runs something, or grants
something, or both, or neither.
### Two stores, and why `database` still is not a name
Two relational stores from different vendors both grant *a database*. They are the sharpest
possible test of the substitutability rule from [`proposal.md`](proposal.md), and they fail it
completely: different wire protocol, different dialect, different driver, different client
library compiled into the consumer.
A consumer declaring `requires: database` and being handed either would break against one of
them. So the name promises what no provider delivers — and now with two real providers in the
catalogue rather than a thought experiment.
*Database* remains a **tag**. It is how a person finds both. It is not how a consumer names what
it needs.
## The same shape, three more times
The message broker has all nine. So does the object store, and so does the image registry. They
differ in what a consumer asks for — a database, a virtual host, a bucket, a repository — and in
nothing structural.
**A substrate service is a service plus a factory.** That is the whole pattern, and there are
four of them. It generalises past the substrate too: anything that grants something per consumer
has this shape, and anything that does not is the simpler case.
But two things differ *between* them, and both matter more than the similarity.
### The broker cannot be managed over the broker
[ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md) makes the broker the
channel every node takes work from, and
[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) makes it the security
boundary — everything a node applies arrives through it.
So the module providing the broker is also **the way modules are managed**. A declaration cannot
be delivered to it over itself, and reconfiguring it is done through the thing being
reconfigured. Nothing else in the catalogue has that property; the store is consumed by the
control plane but is not how the control plane *reaches* anything.
This is exactly what the carried bundle exists for
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)): the broker is raised from
what the host carries, before there is a channel, because there is no other way to raise it.
Recorded here because it is a constraint on *one module*, not a general rule, and a schema with
no way to say so hides it.
### Two modules of identical shape want different instance counts
The broker is one per mesh — a single point of failure and a single point of trust, by decision
rather than by accident. The store cannot be: a node that must keep working while disconnected
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)) cannot depend on a database
somewhere else.
Same nine properties, opposite answers. Which settles something the cases file left open: **how
many instances is not derivable from what a module is.** It is a decision per module, it has to
be declared, and nothing in `provides`, `requires` or `excludes` says it.
### And revocation differs in consequence
Revoking a database leaves data behind until something drops it — a leak, and recoverable.
Revoking a virtual host drops whatever had not been delivered — not recoverable, and silent.
The relation is the same and the blast radius is not, which is an argument for the provider
deciding what revocation means rather than the mesh applying one rule to all of them.
## The assignment is a third thing
Recorded because the operator tried the alternative and abandoned it: **modules were once
node-agnostic**, and it did not survive contact.
The worked example says why. Several of a provider's nine properties are not properties of the
module at all:
- **where its state lives** — a volume on a particular machine;
- **how it is reached** — the same module on two nodes may answer locally, on the network, or
publicly, and that is a per-assignment decision;
- **configuration derived from the hardware** — tuning follows the memory and storage of the
machine it landed on;
- **whether this instance is the one** a given consumer is provisioned from.
None of those belong in the catalogue, because they differ per node. None belong in the node,
because they are about this module. **They belong to the pairing**, and a design with only
modules and nodes has nowhere to put them — which is what "node-agnostic" ran out of.
So there are three entities, not two:
> **a module** · **a node** · **an assignment**, which is a module on a node and carries its own
> configuration
The current system already has this, arrived at the same way: environment values are stored per
module *and per node*, so a module's settings differ between the machines running it.
**This does not put placement back in the manifest.** A module still says what must be true of a
node and never which node ([`proposal.md`](proposal.md)). What changes is that the *result* of
placing it is a thing with its own state, rather than a fact recorded on one of the two ends.
And it makes the composed declaration question from above answerable: a declaration is built
from the module, the node's inventory, and the assignment between them. Three inputs, which is
why two were never enough.
@@ -1,213 +0,0 @@
---
status: active
initiated: 2026-08-26
touches:
- 02-DECISIONS/0005-the-node-host.md
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
- 03-DESIGN/01-to-be/05-the-node-host.md
- 03-DESIGN/00-as-is/05-runtime-and-installation.md
- 01-RESEARCH/011-the-module-graph/00-overview.md
- 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
---
# 012 — The minimum viable node, and adopting what is already there
## What is being investigated
Two questions that turn out to be one:
**What is the bare minimum to run a one-node mesh?** Not the tiers as asserted, but the actual
closure — take the thing that must run, walk what it needs, and the set that comes back is the
answer.
**And how does a machine that is already in use become that?** A candidate node is not empty. It
has a package manager, probably a container runtime, possibly a git installation, each with
configuration somebody chose. The mesh must **own** those, and owning is not the same as finding
them present.
## Why
Building tier 0 reached a wall that looked like a packaging problem and is not.
The host can be told to run a container or install a package. Both need a file — an image, an
archive — and the question was where the host gets it. That framing produced a bad trilemma:
carry everything in the bundle, download at apply time, or have something push the files in
first. Downloading fails on the first node, which cannot fetch the image registry from the image
registry it is trying to start.
> **Qualified by [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md).** The
> reframing below still holds for what a *tailored installer* contains — the missing pieces for a
> given machine. It does **not** have to hold for container images: the installer fetches those
> by digest, because a real machine has a network and the sealed case is the lab.
**The reframing:** the machine is not offline. What matters is *when* the fetching happens. Move
it from apply time to **build time** — build the installer on a machine that has a network,
tailored to the target, and apply it on a target that then needs nothing. That is the same move
the lab already made for its router image, and the same property the delivery design already
claims: what ships is self-contained and a deploy touches no network.
Which makes the interesting question not *where do artifacts come from* but **what is missing
from this particular machine**, and that needs both of the questions above answered.
## What tailoring implies
- **The binary stays generic; the payload is tailored.** One static host per architecture. What
is machine-specific is the bundle it applies. Rebuilding the host per machine would buy
nothing.
- **Detection is the input, not a report.** What the host already reports about a machine —
its profile and inventory — is what the difference is computed against. This is the first use
of stage 1 by something other than a person reading it.
## Adoption
**Simply having a package installed is not enough.** If the mesh manages the container runtime,
it decides that runtime's configuration; a runtime found already installed carries settings
somebody chose, and those cannot be discovered by noticing the binary exists.
So a machine already in use is **adopted**: what is there is read, taken over, and thereafter
generated.
**This was the original path.** [`00-as-is/05`](../../03-DESIGN/00-as-is/05-runtime-and-installation.md)
records adoption of a pre-existing machine's configuration as the original mechanism, since made
legacy and explicitly out of scope for the lab. It returns here for a different reason than it
was dropped for, which is a thing to notice rather than to gloss.
**It creates a state that does not exist today.** [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)
has managed files generated and never edited; adoption needs a one-time import before that rule
starts applying. Three states, and the middle one is new:
> unmanaged → **adopted once** → generated
**And it crosses a boundary just drawn.** [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
says the host never touches what it did not create — the rule that stops a converger deleting
what the mesh never put there. Adoption is the deliberate act of taking ownership of exactly
that. The rule needs a companion rather than an exception: *never, unless adoption made it the
host's*, with adoption being explicit, recorded, and visible in what the host says it owns.
## Nothing is taken over without keeping what was there
**Before adoption touches a file, the original is kept.** Adoption happens on machines somebody
is already using, and the configuration being taken over is configuration somebody chose. A
one-way door on a working machine is not an installation, it is a risk nobody agreed to.
This is a *never* rule rather than a courtesy, and it earns that by the same incident the mesh's
strongest rule already carries: the worst loss in this record came from a tool acting on a path
it did not own. Adoption is that act, made deliberate — which makes the safeguard obligatory
rather than optional.
What that requires, and what remains open: where the copy lives, whether it is recorded in what
the node knows about itself so that adoption is *visibly* reversible, and whether the mesh keeps
it forever or hands it back when it stops managing the thing.
## Adoption produces a briefing, not just a result
Proposed by the operator, and it answers a question this effort had open with two bad answers.
Adoption meets things a script cannot decide. A container runtime configured with one storage
driver and a mesh wanting another. A package pinned to a version somebody chose for a reason.
Local settings the mesh has no opinion about and no business discarding. Silently winning is
wrong in both directions; refusing outright makes a machine in use unadoptable.
**So adoption has two outputs.** What it did — mechanical, recorded, in the node's state. And a
**briefing**: what it found, what it took over, and what it could not resolve, written to be
read by a person or an agent, which is the first thing a session on that node has to work with.
Conflicts are **flagged, not resolved**. That is the same principle the declaration parser
already applies — name every problem at once, to somebody who can act on it — at a larger
scale, and applied to a case where refusing wholesale would be worse than proceeding.
### On conflict, the machine's configuration is kept
Decided by the operator, after first deciding the opposite — recorded that way because the
reasoning for each direction is the useful part.
Where the existing configuration and the mesh's disagree, **what is already on the machine
stays**, the conflict is flagged, and it is reconciled afterwards. Adoption always **completes**
— flags inform, they do not block — and *adopted with open questions* prevents nothing. The
node is a node.
**What this buys.** Adoption becomes non-destructive by construction. The class of conflict that
made the opposite rule dangerous — a storage driver against the filesystem it is actually on, a
data directory pointing at a mount that exists — cannot arise, because nothing tied to the
machine's physical reality is ever overwritten. A machine in use keeps working exactly as it
did.
**What it exposes, which is the mirror of what it fixes.** The mesh's configuration is not only
preference. Some of it is what a module needs in order to function at all. Keeping the machine's
version there produces a module that is installed and does not work — *an installed package is
not a capability*
([04-ISSUES/007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md))
arriving from a direction that issue did not anticipate. And a fleet where every node kept its
own settings is a fleet where a module works on one node and fails on another with nothing in
the mesh able to say why.
**The distinction that dissolves both rules.** Neither direction is right as a blanket, because
the question is not *whose configuration wins*. It is whether the module **requires** the
setting or merely **prefers** it — required contradictions cannot be kept without breaking the
module, and preferences should always yield to what is already there.
That is a property of the module's own declaration rather than of the adoption algorithm, which
makes it one more thing the graph would carry
([research 011](../011-the-module-graph/00-overview.md)). Until modules can say which of their
settings are load-bearing, adoption is choosing a default in the dark, and the default chosen
here is the one that does not break the machine it is adopting.
### The briefing carries an outcome, and the outcome is derived
Proposed by the operator: the report states plainly whether adoption succeeded, partly
succeeded or failed, and each line carries its own severity.
**Each line is marked, and the overall is the worst mark present.** Derived rather than stated
alongside, because two fields written independently drift — and a briefing reading *full
success* while carrying a failed line is exactly the fault this record keeps cataloguing. An
outcome computed from its lines cannot disagree with them.
| Mark | Means |
|---|---|
| `ok` | done, and verified |
| `kept` | a disagreement; the machine's value was kept and somebody should look |
| `unknown` | could not be determined |
| `failed` | could not be done — the node is not what was asked for |
**`unknown` is not a shade of success.** Adoption will meet configuration it cannot parse and
state it cannot read, and folding those into *fine* is the same move as reporting an installed
package as a capability. A thing nobody could determine is a thing nobody can rely on, and it
gets its own mark for the same reason a capability detector reports *why*.
**And this opens something the earlier rule did not cover.** *Flags inform, they do not block*
was decided about **conflicts** — where the mesh chose, deliberately, and the machine still
works. A **failure** is different in kind: not *we chose* but *we could not*. Treating both the
same makes a node where something the mesh needed never happened indistinguishable from one
where a log level differed. Whether a failed line still lets adoption complete is therefore
reopened by adding severity, and is not decided here.
## Adoption as a mode, for the migration
*2026-09-22.* The migration from the predecessor mesh gave the middle state a length. A machine
running the predecessor is **adopted** when the mesh comes up on it — the predecessor's control
stopped, the machine's firewall and files kept in force, the mesh opening what it needs through
them — and stays adopted while its modules migrate one at a time, until the operator **converges**
it. Measured on the control-node, and weighed against the alternatives, in
[*migrating a node that is in use*](migrating-a-node-in-use.md); decided in
[ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).
## Open questions
| Question | Why it is open |
|---|---|
| What is the closure for a one-node mesh? | The skeleton asserts four pinned services. [Research 006](../006-mesh-from-scratch/00-overview.md) already asks whether it is four or five and does not answer. A graph gives a computed answer instead of an asserted one, which is [research 011](../011-the-module-graph/00-overview.md). |
| Is "tier" the same thing as a graph level? | Tiers were named as a bootstrap order. If the closure is computed, tiers may be a derived view of the graph rather than a separate concept — or they may be a coarser boundary that survives for a different reason. |
| ~~What happens when existing configuration contradicts what the mesh needs?~~ | **Answered** — the machine's configuration is kept, the conflict is flagged, and it is reconciled afterwards. |
| ~~Do flags block, or only inform?~~ | **Answered** — they inform. Adoption always completes, and the node is a node. |
| Can a module say which of its settings are load-bearing? | The question that dissolves the conflict rule rather than choosing a side. A setting the module *requires* cannot be kept from the machine without producing something installed and broken; a setting it merely *prefers* should always yield. Until a module can say which is which, adoption is defaulting in the dark. Belongs with the graph. |
| Does a `failed` line still let adoption complete? | *Flags inform, they do not block* was decided about conflicts, where the mesh chose and the machine works. A failure is *we could not*, which is different in kind — and treating them alike hides the worse one behind the commoner one. |
| How is a flagged conflict reconciled, and by whom? | The briefing hands it to a session. What that session is empowered to change, and whether the resolution is recorded so the next adoption does not re-raise it, is undecided. |
| ~~How long is a machine adopted?~~ | **Decided** ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)) — for as long as it is being migrated: a mode per node, recorded, ended by an explicit and previewed flip. |
| Where does the kept original live, and for how long? | Whether it is recorded in the node's state so adoption is visibly reversible, and whether it is returned when the mesh stops managing the thing. |
| What shape is a briefing? | Structured enough to be acted on, prose enough to be read. It is the first thing a session on a new node sees, which makes it an interface rather than a log. |
| Does owning a package mean owning its version? | Owning configuration and owning the package are different scopes. The second means the mesh decides which version is installed, and that decision then has to survive the machine's own package manager updating it. |
| How does a bundle stay true between building and applying? | It is built against a scan of the target. The machine can move between the scan and the apply, so the host has to verify rather than assume — and fail plainly when the bundle no longer fits. |
| What cannot be precomputed at all? | Anything built from source on the target still needs a toolchain and a network at that moment. Tailoring moves that cost rather than removing it, and *minimal viable* has to be honest about what it cannot ship ahead. |
| Does presence differencing understate the gap? | Knowing a package manager is installed does not say it is the version the mesh needs. A difference computed on presence alone is optimistic. |
@@ -1,154 +0,0 @@
# Migrating a node that is in use — adoption as a mode, not a moment
*2026-09-22. Measured on the machine that will be the control-node, which is running the
predecessor mesh today. Nothing below names it; the counts are its own.*
## The question
The mesh replaces a predecessor mesh that is running, on the same machines, with the services
people use. The control-node is decided: it is the machine that already carries the predecessor's
broker and build pipeline. So the question is not *where* the mesh starts but **how a machine
running the predecessor becomes a node of the mesh without its services noticing** — and then how
each of the other machines follows.
## The operator's proposal
Proposed by the operator, 2026-09-22, and the shape this document tests:
1. **Stop the predecessor's control on a machine** — its daemons that write configuration: the
network and firewall configuration above all, which decide what is reachable and what is
blocked. Its services keep running; only the control over their configuration stops.
2. **Bring the mesh up on that machine in adoption mode.** It takes custody of those files and
keeps what it finds in force.
3. **Migrate the modules one at a time**, data preserved, per the cutover procedure.
4. **Move to the next machine and repeat** — adopted first, keeping its local configuration, then
migrated.
5. **When every machine is migrated, flip adoption mode**, and the mesh takes full control of the
configuration it has been holding.
This is the research above made concrete. It keeps the conflict rule already decided here — *on
conflict, what is on the machine stays* — and gives the middle state, *adopted*, a length: not a
one-time import before generating starts, but a mode that lasts for as long as the machine is
being migrated, ended by an explicit act.
## What the machine actually looks like
Measured, read-only:
| | |
|---|---|
| Containers running | 60, all the predecessor's services and their stores |
| The predecessor's control | user-level daemons, separate from the services; none of the 20 running system services is the predecessor's control |
| Firewall | the predecessor's, active: 54 incoming rules and 52 forwarding rules, each served port allowed explicitly — how a default-deny firewall reads |
| Files the predecessor's configuration sync writes | 12, of which 3 are system files (an ssh server drop-in, the package manager's configuration, one service's configuration); the rest are the operator's shell and agent files |
| Per-service configuration | environment and composition files per service, written by the predecessor's service tooling |
**Stopping the predecessor's control stops nothing that serves.** The services are containers and
system units that run without it; what stops is the rewriting of their configuration. Nothing
changes on the machine until something else writes.
## What collides, measured rather than assumed
An earlier note assumed the mesh's foundation could not stand beside the predecessor because both
want the store's and the broker's standard ports. **The measurement says otherwise.** The
predecessor publishes its own store on a non-standard port and its broker on another; the standard
ports the foundation binds for its store and its bus are free.
What does collide:
| The mesh wants | Held by | When |
|---|---|---|
| the registry's port | the predecessor's registry | at genesis — the foundation raises a registry |
| the broker's management port, on loopback | the predecessor's broker | at genesis |
| the private network's port | the predecessor's own tunnel | at genesis on a control-node that is the private network's hub; otherwise when the node is placed on it |
| the web ports | the predecessor's reverse proxy | when the route proxy is assigned |
| the resolver's port | a resolver the predecessor runs | when a resolver module is assigned |
On the control-node, which is the private network's hub, three of these fall at genesis and cannot
be deferred; two only when a particular module is taken, the moment its predecessor stops anyway.
Today the foundation's ports are **fixed**: written in the installer's bundle and in the
catalogue's manifests, so a collision is found when a container fails to bind, not before — and a
port changed at genesis would be changed back when the foundation is adopted as modules, since the
applier recreates a container whose declared spec differs. The private network's address range
must also stay clear of the range the predecessor's tunnel uses; on the machines measured they are
distinct.
## What would break if the mesh came up as it is today
**The firewall.** Genesis loads a base ruleset — drop anything undeclared — in its own table
([ADR 0088](../../02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md)). The
predecessor's firewall is a different table. The kernel runs every base chain registered at the
same hook, in priority order: an accept ends only its own chain and the packet goes on to the next,
and a drop in any is final — whether the other firewall's chains are nftables or legacy iptables.
So the
mesh's base ruleset would drop everything the predecessor's firewall allows and the mesh has not
declared — every web, mail and database port in the table above — the moment genesis ran.
**The files.** The host writes a declared file whatever it finds at the path, reporting it as
updated. A file the predecessor left — the ssh server drop-in, the resolver's configuration — is
replaced the first time a module declaring that path is assigned, before that module's service
has moved.
**The container names.** The applier keys a container on its name. A catalogue module whose
container carries the same name as the predecessor's service it replaces takes that container over
the moment it is assigned: today, *assigning a module is migrating it*, never a preparation.
**The published ports.** The foundation publishes its ports on every interface, and a published
container port reaches the container through the forwarded path, not the incoming one. A firewall
that filters only incoming traffic never sees it. The base ruleset is what keeps the store
unreachable from outside today — and it is the thing that cannot be loaded on this machine.
## The pipeline freezes while the control-node migrates
The predecessor's build pipeline and its coordinator run on the control-node. Stopping its control
there stops the predecessor's updates for **every** machine it manages, until the migration is
done. Their services keep running; they receive nothing new. That is the price of the proposal and
it is worth stating, not a reason against it: the migration is the period in which the predecessor
is being replaced, and it does not need to keep changing.
## What adoption mode has to mean
For the proposal to hold, *adopted* must be a **state the mesh records per node**, not an
intention, and each thing the mesh would otherwise take must say what it does in that state:
- **What is found is kept until its module is taken.** *Found* is precise: present at a declared
path or name with no record in the host's store. A found file or container is held, its original
recorded, until the operator **takes** the module on that node — the cutover, done when the
module's data has moved. Assigning prepares; taking migrates. Without the distinction the rule
never fires: the host only ever sees what assigned modules declare.
- **The firewall found on the machine stays in force.** The mesh loads no table on an adopted node
that drops by default or holds an accept. What it needs open it declares as openings the host converges
*through the found firewall*, on the incoming and the forwarded path, marked as the mesh's and
re-checked on every reconcile so a reload or reboot does not lose them. An accept in a table of
its own would not help: the found firewall's drop would still be final. What a table of its own
*can* do is refuse, and a refusal is final too — so the mesh guards the store and the broker's
management port from everyone but the private network and the machine itself, in a table that
only refuses, ahead of the container runtime's redirect — which the found firewall does not do
and cannot undo. The bus, the registry and the hub's port stay open to
anywhere: a node enrols before it has a private-network address.
- **The foundation's ports are the node's to give** — set at genesis, checked free, and kept as
that node's settings, read everywhere they are used, so adopting the foundation as modules does
not move them back.
- **The flip is per node and previewed** from what is actually reachable — listening sockets and
published ports, not the found firewall's allow list, which does not see what a container runtime
forwards.
## Options weighed
| Option | Verdict |
|---|---|
| Cut the machine over in one go: stop the predecessor's store, broker, registry and proxy, raise the foundation in their place | Rejected. Every predecessor service goes down until it has migrated, and the predecessor's other machines lose their broker. The rollback is restarting the predecessor, which is a recovery, not a step. |
| A separate machine as the control-node | Rejected. Contradicts the decision that the control-node is the machine that already carries the predecessor's broker and pipeline. |
| Converge on joining, as the mesh does today | Rejected. The base ruleset closes every predecessor port at genesis, and found files are replaced before their services move. |
| Make only the foundation's ports configurable and otherwise converge | Rejected as insufficient. It solves the bind collisions and none of the firewall or file ones. |
| Treat assigning a module as migrating it | Rejected on review. The rule that keeps found files would never fire — the host only sees what assigned modules declare — and an assignment on an adopted node would be an outage rather than a preparation. |
| **Adoption as a mode, per node, ended by an explicit flip** | The proposal. It is the conflict rule already decided here, given a duration. |
## What this leaves open
Answered by the decision record this feeds — [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)
— only for the migration's needs. The general questions above stay open: whether a module can say
which settings are load-bearing, what shape a briefing takes, how a flagged conflict is reconciled.
One is newly sharp: the mesh opening ports through a firewall it did not install needs to speak that
firewall. There is one kind on the machines measured; a machine with another is not covered until
someone writes for it.
@@ -1,54 +0,0 @@
---
status: active
initiated: 2026-09-23
touches:
- 02-DECISIONS/0075-two-stores-and-which-provides-what.md
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
- 02-DECISIONS/0071-genesis-builds-from-a-mesh-that-already-exists.md
- 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
- 04-ISSUES/085-the-packages-port-given-at-genesis-is-not-a-setting/00-report.md
- 04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md
---
# 013 — The forge and the registries: what a seat is for, and what it is not
**The question, asked during the first migration:** the mesh runs a forge that serves git and
packages, an image registry, and a bootstrap forge genesis raises before any module exists. Should
there be a mesh-scoped **git seat**, the way there is one for the store and the broker?
**The short answer: no, and the question points at a different hole.** A seat answers *is there
exactly one of you*. Every symptom around the forge is about **an address and a credential nobody
resolves**, which a seat does not answer. The mechanism that would answer it — a provision, with
`serves` and a grant — already exists, is already used for the image store, and is already
declared for packages. It simply has no consumer: the one module that needs it carries a literal
instead.
See [the survey](the-survey.md) for what the code does today, with evidence.
## What was found
- **A seat does exactly two things**: it refuses a second claimant at resolution, and in one place
it answers *where is the broker*. It is a resolution-time predicate; no host ever hears of it.
- **Four mesh-scoped seats exist**, all named after a singular server the mesh runs **for its own
working** — controller, store, broker, catalogue. A forge is an application the world made, not
part of that set.
- **Nothing in the mesh requires git.** There is no provision for it, and the forge's git service —
over https and ssh — is declared nowhere. Only its npm half is a provision.
- **`package-registry` is a fully built provision with zero consumers.** The builder, its only
real consumer, bypasses it with a file naming the module, the address `127.0.0.1` and the port.
- **The builder is told where source lives, per build, by a person.** The clone URL is an opaque
string; each module remembers its own; no credential is ever attached; nothing polls a forge.
- **A seat would forbid something the mesh should allow**: a second forge — one for the mesh's own
source, one for something else — which ADR 0075 already argues for against a node-scoped claim.
## What this leaves open
- Should the forge's git service become a **provision** (`source-forge`), so the builder resolves
the address and a credential the way it already resolves the image store? That is what would let
a forge move, be renamed, or be replaced without editing every module's stored URL, and it is the
unanswered half of [issue 085](../../04-ISSUES/085-the-packages-port-given-at-genesis-is-not-a-setting/00-report.md).
- The obstacle is known and written down: such a requirement **may go unanswered during genesis**,
and the mesh has no optional requirement. Deciding that is the real work.
- Should the mesh learn when a source moves? It records the fact and never discovers it: nothing
polls, and there is no receiver for a forge's push. `build --behind` answers a question only a
person can currently make true.
@@ -1,130 +0,0 @@
# The survey — seats, the forge, the registries, as the code has them
*2026-09-23. Read from the code and the records, not from memory. Every claim here was checked
against a file.*
## What a seat is
The glossary calls a seat *a named position at a scope with a capacity*, and a claim *a module
taking a spot on it*. **Capacity does not exist in the code.** There is no capacity field and no
bench; every claim is exclusive. The "shared seat" of the design is the word `provides` wearing
that name.
Claiming does two things and no others:
- **It refuses a second claimant when a node's modules are resolved** — within one node at any
scope, and against every other node for mesh and site scope.
- **It answers one question, once:** the controller finds where the broker is by looking for the
module claiming the broker's seat.
No host is ever told about a seat. It is a resolution-time predicate.
Two mechanical asymmetries matter. A mesh-scoped seat's exclusivity is defended only by nodes
that *resolve*: a node whose modules fail to resolve contributes nothing, so a broken node does
not hold its seat against a second claimant. And a site-scoped seat does nothing at all when
either node has no site.
## The seats that exist
Twelve claims, twelve names. Four are mesh-scoped: the controller, the store, the broker, the
catalogue. All four are things the mesh runs **for its own working**, and three of them are raised
by genesis and adopted in place — which is the argument of the record that named them.
The node-scoped ones split in two: a **role on this machine** (the build machine, the packet
filter, the intrusion prevention, the private network) and **a scarce machine resource** (the
resolver's configuration file, the DNS port). The second kind is a claim for the reason a port is:
there is one of it on the machine.
Two oddities worth stating:
- The image store claims its seat at **node** scope while providing its service at **mesh** scope.
The record that discussed this left open whether it should hold that claim at all.
- **Nothing claims the two public ports.** The proxy binds them and claims nothing, which is the
hole the route handover fell into.
## The forge, and what it serves
The forge serves three things: git over https, git over ssh on its own port, and an npm registry.
**Only the npm half is in the mesh's vocabulary** — the forge provides `package-registry` and says
where it answers. Git is served and declared nowhere: no provision, no `serves`, nothing that can
require it. The only trace is the public label in its route contribution, which the mesh is
explicitly not meant to interpret.
The image store is a separate module and a separate provision, plain HTTP, trusted because it is
reachable only over the private network.
A second module also provides `package-registry`. Two providers of one provision is not a refusal
but an ambiguity, and the mesh already has the command that settles it: pin one.
## Where the builder's three addresses come from
| What it needs | How it finds it |
|---|---|
| the broker | a sealed secret of its own |
| the image store | **a real provision binding**, resolved by the mesh |
| the package registry | **a file in its own manifest**, naming the module, `127.0.0.1` and the port |
| the source repository | nothing at all — a person types a clone URL per build |
The installer says this plainly in a comment: *the one binding nothing resolves: the builder dials
the forge by a number it carries.* Genesis can override the port of that literal, and nothing
else: not the forge's identity, not its address.
`package-registry` therefore has **zero consumers**. The builder does not require it. The
provision is fully built — two providers, grants, a served description including the registry's
path — and nothing asks for it.
## How a module's source is found
It is not found; it is supplied. The clone URL arrives as an argument, travels to the build machine
and reaches `git clone` unexamined. No credential is attached, so a private forge works only if the
build machine's own git configuration already authenticates. Each module remembers the URL it was
built from, so there are as many forge addresses as modules, each frozen at whatever was typed.
At genesis there are four independent source URLs, one per flag, unrelated to each other.
If the forge moves or is renamed: every stored URL goes stale independently, each failing at its
own clone, and there is no command to re-point them. Genesis keeps working, because it takes URLs
as flags — which is the existing record's position: the forge is reached *by a name outside the
mesh*, and failover is repointing that name.
## What genesis's forge is
A bare container run before any module exists, because the toolchain resolves the mesh's own
packages by version from an npm registry. It holds **no seat, no manifest, no provision**. The
mesh is told one thing about it — a port — and only when that port is not the default. **On an
ordinary genesis the mesh never learns the bootstrap forge exists.**
Its successor module differs from it in five ways: container name, network, data directory, the
address it is told to call itself, and — contradicting a comment that says they are pinned
identically — **the image digest**. So assigning the module raises a second forge beside the first.
A seat cannot fix that. A seat is checked between manifests; the bootstrap forge has no manifest,
so no seat can see it. The check that is actually wanted — same name, same data, same network — is
between an installer constant and a manifest.
## Would a git seat be coherent?
In form, yes: add the claim and a second forge is refused. In effect it buys one refusal nobody
has hit, and forbids an arrangement the mesh should allow — a second forge for something other
than the mesh's own source.
Measured against the four mesh seats, it does not fit: those are singular servers the mesh runs
for itself, and a forge is an application. Nothing would *use* it, because the only seat consumer
answers "where is the broker", and a forge address is not one fact: a module's source is a
repository **and a path and a ref**.
## What the code already supports and nobody uses
- **`package-registry` with no consumers.** The builder's literal file is a hand-rolled copy of
the binding the mesh would have written for it.
- **Pinning a provider** already expresses *this* forge, of several, without forbidding the second.
- **Grants** on the forge are the credential mechanism that would hand the builder an account.
Today the installer creates that account directly, because nothing requires the provision and so
the grant has no consumer to write for.
## The reading
The question *should there be a git seat* is the wrong shape for the problem under it. Every
symptom — the literal address, the port that would not follow a node, the takeover that is not a
takeover — is about **an address and a credential nobody resolves**. A seat resolves neither. The
provision mechanism does, and is already built.
@@ -1,108 +0,0 @@
---
status: graduated
became: 02-DECISIONS/0106-the-bus-is-nats.md
initiated: 2026-09-23
touches:
- 02-DECISIONS/0002-nodes-communicate-over-a-broker.md
- 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
- 02-DECISIONS/0041-events-are-a-relationship.md
- 02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
- 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
---
# 014 — The bus on NATS: replace the broker, and when
**The question, asked mid-migration:** the operator wants the mesh's bus — today an AMQP broker —
replaced by NATS, with everything the broker does today. Do we finish the migration on AMQP and
move to NATS after, or go to NATS directly?
**The short answer: decide NATS now, build it in the lab in parallel, and cut the mesh's bus over
in one rehearsed rollout — after the migration's core is done and never underneath it.** "First
everything on AMQP" is already the state and costs nothing more: the predecessor's broker was
merged into the mesh's tonight, and every module converted from here on targets the sdk's broker
contract, which names no protocol. The only code that speaks AMQP is the mesh's own, in three
places, and it is swapped once.
## What was measured
**Where AMQP is spoken** (non-test files): the controller's `internal/link` (5 files, ~2.4k lines
with tests) and the builder's main; the host's `internal/link` (~1.5k); the tool runtime's
`broker-amqp.ts` (~600 with its main). **The sdk speaks none** — its `Broker` is `request`,
`handle`, `publish`, `subscribe`, `close` ([ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)
put the client in the runtime for exactly this). **Every module's tools and events go through the
sdk**; one module (`amqp-ping`, a probe) talks AMQP on purpose.
**What the bus carries:** a control queue (`control`, `.upgrades`, `.catchup`), one queue per node
(`node.<name>`), `builds`, the enrolment/report/alive/built flows, a topic exchange of events
(`mesh.events.<module>.<event>`, dead-lettered to `mesh.events.dead`), an RPC exchange (`mesh.rpc`)
and per-tool service queues (`serve.<module>.<tool>`), plus an MQTT exchange.
**What it relies on:** TLS for the bus a node enrols over; prefetch with reject/nack so the control
queue **holds messages unacknowledged while the store restarts** and retries
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)); publisher confirms and
mandatory routing; a dead-letter exchange; **per-module accounts scoped by `emits`/`consumes`**
([ADR 0043](../../02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)),
minted through the broker's management API; the broker as a module holding the `mesh-broker` seat
([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)).
**The predecessor's world**, now on the same broker: ~50 AMQP connections per machine from four
machines, 595 queues, four vhosts, six users. All of it AMQP, all of it retiring module by module.
NATS speaks no AMQP: that world cannot move; it does not need to.
## How NATS answers each of those
| The mesh relies on | NATS | Note |
|---|---|---|
| topic routing keys | subjects with wildcards | same shape (`mesh.events.>`) |
| RPC and tool invocation over an exchange + reply queue | request/reply, native | simpler than today |
| competing consumers | queue groups | same |
| durable control queue, hold-unacked-and-retry, catch-up | JetStream: streams, durable consumers, ack/nak with delay, replay | the 0083 guarantee moves to JetStream; core NATS alone is at-most-once and would not do |
| dead-letter | max-deliver + advisories, or a stream fed from them | different mechanism, same effect |
| TLS bus | TLS | same |
| accounts scoped by emits/consumes, vhosts | accounts (isolation) with users and per-subject publish/subscribe permissions | stronger than today; the controller writes an auth config the host declares and the server reloads, instead of calling a management API |
| management API | `nats` CLI and an HTTP monitoring endpoint | no vhost concept — accounts instead |
| MQTT | built in | same |
| a module seat `mesh-broker` | unchanged — the seat is the server, the module changes | ADR 0079 |
| multi-node | clusters and leaf nodes | not needed now; a leaf per node is a later question |
Nothing the mesh needs is missing. The differences are in the shape of durability (JetStream must
be declared, streams and consumers are objects) and of accounts (configuration, not API calls).
## The cost, honestly
The bus is the mesh's nervous system. Moving it means: a new `nats` module in the catalogue taking
the `mesh-broker` seat; the controller's and the host's link packages rewritten; the tool runtime's
client swapped behind the unchanged contract; enrolment, reports, builds and the guard's ports
re-derived; the lab beds that prove the bus (store window, enrolment, upgrades) re-run on the new
one; and eight decisions amended or superseded. Weeks, not days — and none of it can be done
halfway on a live mesh: the controller, every host and every tool runtime move together.
## The sequencing question, answered
1. **NATS first, directly.** Stalls the migration for the length of the build; the mesh's bus
changes under a half-migrated node; the predecessor's clients still need AMQP, so a second
broker runs anyway, plus a bridge for whatever crosses. Rejected.
2. **Finish on AMQP, NATS after.** Wastes nothing — no module written from here on speaks AMQP —
but leaves the decision unmade while modules are written, and the runtimes idle. Adequate.
3. **Decide NATS now; build it in the lab in parallel; cut the mesh's bus over in one rehearsed
rollout after the core is migrated.** The predecessor's clients never notice: their broker is
the one the mesh adopted, kept as a compatibility module with an end date — the day the last
AMQP client is gone. **Recommended.**
## What this leaves open
- The **decision itself**, as a record: the bus is NATS; the AMQP broker becomes the predecessor's
compatibility broker and retires with the last AMQP client. Written when the operator says so.
- **JetStream's shape for the control plane**: one stream per concern (control, nodes, builds,
events) or one with subjects; retention; what the store window guarantee looks like as ack-wait
and nak-delay. Measured in the lab, not designed on paper.
- **Accounts as configuration**: the controller writes users and permissions into a file the host
declares, reloaded on change — which is the [ADR 0102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
discipline applied from the start — or the JWT/operator model. The first is simpler and matches
how the mesh already writes everything.
- **The MCP bridge** the operator asked for the same day is written against the sdk's contract, so
it moves with the bus and is not written twice.
- Whether a **leaf node per machine** replaces the hub-and-spoke bus later — out of scope here.
@@ -1,142 +0,0 @@
---
status: active
initiated: 2026-09-24
touches:
- 02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
- 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
- 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
- 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
- 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 03-DESIGN/00-as-is/03-provisioning.md
- 03-DESIGN/01-to-be/07-the-foundation.md
- 04-ISSUES/113-the-object-stores-images-were-withdrawn-upstream/00-report.md
---
# 015 — The object store after MinIO: which S3 implementation, and how the data moves
**The question.** The mesh's object store is MinIO. Its community edition is archived upstream,
its server and client images have been deleted from every public registry, and the pinned release
is four and a half years old and will never be patched
([issue 113](../../04-ISSUES/113-the-object-stores-images-were-withdrawn-upstream/00-report.md)).
Which S3-compatible implementation replaces it, and what is the migration track for the data and
the provisioning model that sit on top of it?
**Why now, and why not sooner.** Nothing is on fire: nodes that already hold the images keep
running, and issue 113 establishes that the deploy path tolerates an unfetchable-but-present
image by design. The forcing function is not an outage but a one-way door — **no node that does
not already hold the images can ever provision the module again**, so the mesh's ability to stand
a node up from its declarations is already broken for this module, and silently.
**The direction is not a departure from the design; it is the design.** The foundation document
already states the commitment:
> The dependency is on the **protocol**, not the product: AMQP for the bus, S3 for the object
> store, the OCI protocol for the registry. That is what keeps the naming safe rather than a
> commitment that cannot be revisited.
The object store is also **not** a foundation service — ADR 0028 removed it, and it is an
ordinary module required through the module graph by whatever wants one. (The "exception that is
not a swap" in that passage is the relational store, whose provisioning model borrows PostgreSQL's
own meaning of databases, roles and schemas. The object store carries no such coupling: a bucket
is a bucket.) So this effort is an instantiation of an existing principle, not a redesign — which
is the cheapest kind of decision to make and the strongest kind to cite.
## What the replacement has to carry, measured
Taken from the module's manifest, its composition, its tool surface, and a search for its
consumers across the catalogue — not from assumption.
| Requirement | Evidence in the module today |
|---|---|
| S3 API | The protocol every consumer speaks; already the design's stated dependency. |
| ~~OIDC login against the mesh's identity provider~~ | **Struck 2026-09-24. Not a requirement, and it never worked.** Six variables are wired and an entrypoint blocks on the provider, which reads as a live feature. The module's own hook comment records the end state as *"policy claim missing"* — a failing login. See [01](01-candidate-comparison.md). |
| **Per-application access keys, each scoped to a bucket** | The real requirement. A "user" of the store is normally an application; the mesh already mints a credential per provisioned bucket. |
| **One live consumer using it as opaque primary storage** | A file-sync application, since early 2023: objects named by internal id, metadata in its own database. Highest-risk consumer — a live copy drifts, and its bucket name must be preserved. |
| Erasure-coded multi-node topology | Four server nodes with two data directories each, behind a load balancer. |
| A single-node form | Declared as a flavour, for development and small nodes. |
| Buckets as a typed provision | The module declares a provision type of `bucket` on a named network; the mesh mints the credential and the provider creates it (ADRs 0048, 0084). |
| A tool surface | Bucket create/list/delete, object list/info/delete, presigned URL, and provisioning. |
| A console | Published on its own subdomain through the reverse proxy, with an unlimited request-body middleware for uploads. |
**Consumers, counted:** one application module, one capture module that takes a private bucket per
node, one workflow module's tools, and the delivery/rescue internals of the shared library. The
surface is small — the cost is concentrated in the provisioning handler, the tool handlers and the
OIDC story, not spread across the catalogue.
## Candidates
**Four candidates, not three.** The comparison was briefly narrowed to SeaweedFS on the strength of
console single sign-on; that axis turned out not to be a requirement, and the incumbent's own
maintained fork had been omitted altogether. Both errors, and why they happened, are recorded in
[01 — the candidates measured](01-candidate-comparison.md), which carries the evidence and the
requirement-by-requirement detail.
In short, and only in short:
- **The maintained fork of the incumbent** — the community edition was archived and its images
deleted, but a fork publishes, tracks CVEs, and preserves the on-disk format, S3 API and
environment surface. Costs **an image reference** where every other option costs a data
migration, two rewrites and a maintenance window. Does not end the dependence on an abandoned
codebase; buys time to choose deliberately.
- **Garage** — its permission model *is* the requirement (per access key, per bucket), its admin
API is the closest match to how the mesh provisions, and the highest-risk consumer is
first-party documented against it. Remaining cost: no object versioning, no server-side
encryption or object locking, partial lifecycle — **unmeasured against the ten buckets, and the
one thing that could still disqualify it**.
- **SeaweedFS** — longest field record and erasure coding. Its console sign-on is a paid feature,
which is now beside the point. What weighs against it is narrower: its S3 surface is a gateway
translating onto its own file-system API, with no first-party support for the opaque consumer.
- **RustFS** — closest in shape to the incumbent, so the least porting. But it reached general
availability eight days before this was written, and carries an open defect in the credential
path. Two earlier claims about it are corrected in 01: it is **not** a drop-in that retains
existing data.
- **Ceph RGW** — remains rejected as disproportionate where the object store is an ordinary
module rather than a platform.
**This is now two decisions, not one:** whether to repoint to the fork or migrate, and — if
migrating — to which. Repointing does not foreclose migrating, which is the argument for taking it
first. On the corrected requirement the migration ranking is Garage, then SeaweedFS, and not yet
RustFS. Two measurements gate any graduation: **which S3 endpoints the consumers actually call**
(Garage cannot be ranked fairly until counted), and **whether the fork can read the incumbent's
on-disk format in place** — tested on a copy, because the migration between them is one-way. Both
are in [01](01-candidate-comparison.md#what-is-still-unmeasured).
## The migration track, in outline
Data movement is the easy half, and deliberately reversible.
1. **Stand the replacement up beside the incumbent**, on its own ports, its own provision type and
**its own data directory**. Nothing removed. The data directory matters: reusing one the
incumbent already holds would put a fresh single-drive store on top of a live erasure set.
2. **Copy bucket by bucket with a neutral tool.** `rclone` rather than the incumbent's own client
— the client has been withdrawn upstream too, so building the migration on it would inherit
the same dependency this effort exists to remove.
3. **Verify per bucket** — object counts and checksums, not a transfer exit code.
4. **Repoint consumers through the connection the module already publishes.** Consumers read an
API URL from the module's declared connections rather than addressing the store directly, so
the cutover surface is that value plus the provisioning and tool handlers.
5. **Freeze writes, final incremental sync, flip**, and keep the incumbent read-only as the
rollback until confidence is earned. For the opaque consumer this is **not optional and not
instant**: it stores objects by internal id with metadata in its own database, so a copy taken
while it runs will drift. It needs a maintenance window for the final sync, and the window is
proportional to 82,496 objects rather than to 230 GiB.
6. **Retire**, and only then remove the module.
The genuinely new work is not the copy. It is the **provisioning handler** and the **tool
handlers**, both written against the incumbent's admin API. *The OIDC wiring was previously listed
here and is struck: it is not a requirement and it never worked.*
## Open questions
- ~~How much of the OIDC requirement survives, and in which build?~~ **Answered, and it was the
wrong question.** The console requirement does not exist, and the login it referred to never
worked. What replaced it: which S3 endpoints consumers actually call, and whether the fork reads
the incumbent's format in place.
- Does the mesh's bucket provision translate to the candidate's identity model without weakening
what ADR 0049 says about a consumer's identity fitting the tightest backend?
- Should this effort also answer issue 113's general question — mirroring third-party images into
the mesh's own registry — or is that a separate decision? Replacing one withdrawn product with
another unmirrored upstream leaves the same one-way door in place, just further from the hinge.
- Is the four-node erasure-coded topology still warranted, or was it inherited? Worth re-asking
while the product is being chosen, rather than reproducing a shape by default.
@@ -1,176 +0,0 @@
# 015 / 01 — The candidates measured
*Rewritten 2026-09-24. An earlier version of this document ranked the candidates on whether they
preserved single-sign-on to the object store's **console**. That was the wrong axis — it is not a
requirement — and a fourth candidate was missing entirely. Both errors are recorded at the end,
because how a comparison came to be ranked on the wrong thing is worth more than the ranking was.*
## The requirement, corrected
Taken from the operator and from the running system, not from the module's shape.
**A "user" of the object store is normally an application.** The requirement is therefore
**per-application access keys, each scoped to its own bucket** — not per-human single sign-on. The
mesh already works this way: it mints a credential for every provisioned bucket, and the consumer
reads an endpoint from the module's declared connection rather than addressing the store directly.
**The console is not a requirement.** It was the axis the previous version ranked on, and it should
not have been.
**The identity-provider login never worked.** The predecessor's module wires six OIDC variables and
blocks startup until the provider answers, which reads like a working feature. It is not: the
module's own hook comment records the end state as *"policy claim missing"* — a **failing** login,
written up as progress because it proved the provider had registered. The identity provider emits no
such claim, nothing in the module creates the mapper, and the configured scope alone would not carry
a custom one. Two days of logs show no genuine login attempts, only internet scanners failing on an
STS API version. **Nothing should be carried forward on the assumption this works**, and no
candidate should be credited or penalised for matching it.
**One consumer is live, opaque, and holds real user files.** A file-sync application has used the
store as its **primary storage** since early 2023: objects named by an internal id, with all
metadata in its own database. Three consequences — a copy taken while it runs will drift, its bucket
name must be preserved or its database references break, and it is the highest-risk consumer of the
lot.
## What is actually stored, measured
| | |
|---|---|
| Logical | **230 GiB, 82,496 objects, 10 buckets** |
| Raw on disk | **468 GiB** — eight drive directories at 59 GiB each |
| Implied scheme | 468 ÷ 230 = **2.03×**, confirming erasure coding at half parity |
| Headroom | ~1.3 TiB free on the filesystem holding it |
**All eight "drives" are directories on one filesystem on one machine.** The erasure coding is
therefore not buying independent-drive redundancy; the real failure domain is the array underneath,
which has its own. This single fact decides more of the comparison than any product feature: a
scheme's redundancy model is close to irrelevant here, and what remains is its storage overhead.
At 230 GiB with 1.3 TiB free, **storage overhead is not a deciding cost either.** Replication at
three copies would run ~690 GiB against the present 468 GiB — about **+222 GiB**, comfortably
absorbed. Erasure coding at a wider stripe would *save* roughly 146 GiB. Both are rounding errors
against the headroom, and neither should decide this.
## The candidates
Four, not three. The previous version omitted the first.
### The maintained fork of the incumbent
The community edition was archived upstream and its images deleted
([issue 113](../../04-ISSUES/113-the-object-stores-images-were-withdrawn-upstream/00-report.md)),
but **a fork is maintained and publishing** — `pgsty/minio`, from the Pigsty project. It restores
the console stripped from the community
build, rebuilt image and package distribution, tracks CVEs, and states that it preserves the on-disk
format, the S3 API and the environment-variable surface. Verified by pulling it: it reports a
current release, permissive-to-copyleft licensing unchanged from upstream, and identifies itself as
a community fork. Adoption is real — the server image has been pulled three quarters of a million
times.
**Why it reorders the comparison.** Every other candidate costs a data migration, a provisioning
handler rewritten against a different admin API, a tool surface ported, and a maintenance window for
the opaque consumer. The fork costs **an image reference**. It also closes the issue's one-way door:
a node holding nothing can provision the module again, and patches resume.
**What it does not do** is end the dependence on a codebase its original authors abandoned. It is
maintenance mode, largely one project's effort, with no new features intended. It buys time to
choose deliberately rather than under pressure — which is worth a great deal, and is not the same as
a decision.
### Garage
**The best fit for how the mesh provisions.** Its permission model is *per access key, per bucket,
read/write/owner* — which is the requirement above stated verbatim rather than approximated. Its
admin API is a first-class REST surface with tokens scopeable to exactly the two operations a bucket
provision performs. The opaque consumer is **first-party documented** against it, for primary
storage, including client-side encryption support.
Its previously-recorded penalties mostly dissolve under the corrected requirement: it has no console
and no identity-provider integration, neither of which is wanted; and it replaces AWS-style ACLs and
bucket policies with its own per-key-per-bucket model, which is the thing being asked for.
**What genuinely remains.** It replicates rather than erasure-codes — immaterial at this volume and
on a single array, as above. It does **not implement the full span of S3 endpoints**: object
versioning is absent, object locking and server-side encryption endpoints are absent, and lifecycle
is partial. **Whether any of the ten buckets depends on those is unmeasured, and it is the one thing
that could still disqualify it.**
### SeaweedFS
Longest field record of the group, permissive licence, erasure coding, and identity-provider
integration on the S3 API through token exchange. **Its console sign-on is a paid feature** — the
admin UI itself is open, its identity integration is not. That finding is what falsified the
previous version's narrowing, and it is now largely beside the point, since the console is not a
requirement.
What weighs against it here is narrower and more specific: its S3 surface is a **gateway
translating onto its own file-system API**, with acknowledged divergence from AWS behaviour at the
edges, and there is no first-party documentation for the opaque consumer. For a store already
holding real user files in an opaque layout, first-party support is worth more than a feature list.
It also carries more moving parts than a single-machine deployment needs.
### RustFS
Closest in shape to the incumbent — a similar admin API and client compatibility, so the existing
handlers would port with least effort — under a permissive licence, with erasure coding and a
console that does integrate an identity provider.
Two things were recorded about it earlier that were **wrong, and are corrected here**: it is *not* a
binary-level drop-in that retains existing data (API compatibility and on-disk compatibility are
separate paths, and the on-disk one is preview-scoped with documented encryption limits), and it
therefore offers no shortcut around the migration. It also carries **an open defect in the exact
area the mesh depends on** — an access key created by an identity-provider user reported denied on
all S3 operations.
Decisively for now: **it reached general availability eight days before this was written.** For a
component holding 230 GiB of real user files, field record is a feature, and it does not have one.
### Ceph RGW
Remains rejected, for the reason already recorded: disproportionate where the object store is an
ordinary module rather than a platform.
## Where this leaves it
**The decision is no longer "which product replaces the incumbent".** It is two decisions, and they
can be taken in either order but should not be confused:
1. **Repoint to the maintained fork, or migrate now?** Repointing is an image reference and it
closes the issue. Migrating now costs a data copy, two rewrites and a maintenance window, and
buys independence from an abandoned codebase sooner.
2. **If migrating, which?** On the corrected requirement the ranking is **Garage first** — its
permission model *is* the requirement, its provisioning API is the closest match, and the
highest-risk consumer is first-party supported. SeaweedFS second, on field record, with a
translation-layer caveat that matters more here than its feature list. RustFS not yet, on age.
Taking (1) does not foreclose (2), and that asymmetry is the argument for taking (1) first.
## What is still unmeasured
1. **Whether any of the ten buckets needs object versioning, server-side encryption or lifecycle.**
This gates Garage specifically and nothing else here answers it.
2. **Whether the fork's release can actually read the incumbent's on-disk format in place.** The
format is claimed compatible across a multi-year gap; the migration between them is one-way, so
this is tested on a copy or not at all.
3. **Whether the opaque consumer's maintenance window is acceptable**, and how long it actually is
at 82,496 objects.
4. **Whether the eight-drive erasure-coded shape is warranted at all.** The evidence above says it
is not buying what it appears to: eight directories, one array, one machine. It looks inherited.
**Nothing graduates to a decision before 1 and 2.**
## Two errors in the previous version of this document
Recorded because the shape of both survives anonymisation and neither is unique to this effort.
**It ranked on a requirement that did not exist.** Console single sign-on was treated as the axis
because the module's configuration showed it wired up, and a wired-up configuration was read as a
used feature. It was neither used nor working. *A configured feature is not an observed one*, and
the evidence needed was the operator's answer and the logs — both cheap, neither consulted before
the ranking was written.
**It omitted the incumbent's own fork.** The whole effort began because an upstream withdrew its
images; whether anyone had continued that upstream was the first question to ask and it was not
asked. The candidate list was assembled from a search for *alternatives*, which by construction
returns things that are not the incumbent. *When a dependency dies, "who took it over" precedes
"what replaces it".*
@@ -1,57 +0,0 @@
---
status: graduated
became:
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
- 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
initiated: 2026-09-26
touches:
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
- 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
- 03-DESIGN/01-to-be/13-credentials-and-their-rotation.md
- 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
- 04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md
---
# 016 — How a credential can be rotated
**What.** Which rotation mechanisms the mesh's providers can actually support, measured against
their code rather than assumed. Every provider in the catalogue was read, found by listing every definition that provides something: how it names what it
makes for a consumer, what its remove destroys, whether it re-applies a password, whether its
backend can hold two secrets for one login or two logins on one resource, and how its own
administrative credential is set. The consumer side was read too: when a module reads a secret, and
what makes it read a new one.
**Why.** [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), as first drafted,
chose *overlap*: add a second login beside the first, move every reader, then remove the old one,
"through the adapter's existing create and remove", with "no consumer changes". A review showed that
claim false. In most providers the consumer's data is named after its login, and remove drops the data
with the login. Overlap as written would have deleted every consumer's database on its first
rotation. The mechanism has to be chosen on what the providers do.
**What it touches.** Rotation in 0113 and [to-be 27](../../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md),
which [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md) decided on
these findings. The identity budget in
[ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md), if a consumer
gets two logins. The rotation already implemented, which [to-be 13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
describes.
**Documents.**
- [01 — The providers](01-the-providers.md): the survey, one row per provider, and what it shows.
- [02 — The readers](02-the-readers.md): how a secret reaches a running process, and what already
recreates it.
- [03 — The options](03-the-options.md): each rotation mechanism against those facts, and a
recommendation.
**Finding, in one paragraph.** All nine credential providers already re-apply a consumer's password
in place on every create, and the controller's `rotate` command relies on that. It is a working
rotation with a stated window. Eight of the nine name the consumer's resource after its login, and five
destroy the consumer's data when they remove the login. The harness, keyed by login, would do the same
on any change of login. Only one backend holds two passwords on one login, and two more hold several
tokens. Eight backends can grant two logins the same rights over one resource; the ninth can give one
login a second token. So every provider can hold **two credentials** over one resource, but only after
each adapter separates *the consumer's resource* from *the credential that reaches it*. In postgres
that also means the resource belongs to a role no login owns. Administrative credentials are a
different case. They have one party and a fixed name, and five backends take them only at first
initialisation, so changing one needs the old and the new value at once.
@@ -1,103 +0,0 @@
# 01 — The providers
Read from the catalogue's main branch: each provider's provisioner adapter (`create`, `remove`),
the client functions they call, and each definition's own credentials. The providers were found by
listing every definition that provides something and has a provisioner, not from memory. A first pass
of this survey worked from memory and missed one, mailu.
**The provisioner harness** in `mesh-sdk` calls `create` for a consumer when its contribution appears
or changes (its login, password or values), and after the provisioner restarts. It calls `remove` for
a login it applied earlier in the same process that is no longer contributed. Its record of what was
applied is kept in memory and keyed by login. Two things follow:
- a consumer whose derived login changes is removed under the old login and created under the new one,
in one pass;
- a contribution that disappears while the provisioner is down is never removed, and is left behind.
## The credential providers
`login` is the consumer's derived identity, which the adapter receives as `as`
([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
| provider | the consumer's resource is named | remove destroys | create re-applies the password | two secrets on one login | two logins on one resource |
|---|---|---|---|---|---|
| postgres | a database named `login`, owned by the role `login` | the database and the role | yes, `ALTER ROLE … PASSWORD` when the role exists | no: a role has one password | yes, but only through a role that cannot log in owning the database, with each login working as it. Otherwise whatever one login creates is its own, and dropping that login means handing its objects over first. Not done today |
| mssql | a database named `login`, with the login mapped into it | the database and the login | yes, `ALTER LOGIN … WITH PASSWORD` | no: a login has one password | yes, two logins mapped to users in `db_owner`. A user owning a schema cannot be dropped, and a login with an open session cannot. Not done today |
| mongodb | a database named `login`, with a user holding `dbOwner` | the database and the user | yes, `updateUser` with the new password | no: a user has one credential | yes, two users with `dbOwner` on one database. Not done today |
| redis | the key prefix `login:` on an ACL user named `login` | the user, **not** its keys | yes: `ACL SETUSER … reset … >password` replaces all of them | **yes**: an ACL user holds several passwords, added with `>` and removed with `<`. Today's `reset` discards all but the new one | yes, two users on one key prefix, once the prefix is not the login |
| minio | a bucket derived from `login`, and a service account whose access key is `login` | the access key; the bucket **only if empty**. A bucket holding objects is left, and the failure logged | yes, by removing the access key and adding it again, which leaves a moment with no key | no, but an access key *is* the login: a second key is a second login | yes, two service accounts with one bucket policy. The access key is capped at 20 characters |
| lavinmq | a virtual host named `login`, and a user named `login` with permissions on it | the virtual host, with any queued messages, and the user | yes, the user is written again with the password | no: a user has one password | yes, permissions for two users on one virtual host |
| mosquitto | a client named `login`, with a role named for it on the topic prefix `login/#` | the client and its role | yes, the password is set when the client exists | no: a client has one password | yes, two clients holding one role, once the prefix is not the login. The MQTT client identifier is chosen by the consumer, not tied to the login; a duplicate one takes the older session over |
| mailu | a mailbox `login@domain`, unless the consumer contributes its own account name | the mailbox with its mail, for a login-named one; a contributed name is left for an operator | yes, the password is set when the user exists | no for the password; a user can hold several authentication tokens, per the backend's documentation | **no**: a mail user *is* its mailbox |
| gitea (npm) | a user named `login` on a team of an organisation that owns every package | the user; **packages survive**, because the organisation owns them | yes, the user's password is set on every run | no for the password; a user can hold several access tokens | yes, trivially: a second member of the same team |
## The other providers
| provider | answers with | credential |
|---|---|---|
| umami | a website, found by its public name | none. The site id it makes has no way back to the consumer today |
| cloudflare-dns | a public name derived from `login` | none handed to the consumer; its own API token is an operator value |
| showcase | a route | none |
| mesh-vault | custody: it records and withdraws sealed values in a ledger | it holds secrets; it makes none today |
verdaccio provides the npm registry too, and has no provisioner.
## Each provider's own administrative credential
| provider | identity | how the backend takes it |
|---|---|---|
| postgres | a fixed superuser | from a file **only at first initialisation** |
| mssql | `sa` | from the environment at first setup. The image documents no file form, and the definition records that as a declared exception |
| mongodb | a fixed `root` | from a file **only at first initialisation**, when the data directory is empty |
| mosquitto | a fixed admin client | seeded into the broker's dynamic-security file **once**; the seeding step skips when the file exists |
| lavinmq | a fixed admin name | per its own bootstrap code, **only on a first boot** with an empty data directory. No resource in the definition runs that bootstrap; what sets it on a running mesh is outside the catalogue |
| redis | the default user | from `requirepass` in a configuration the mesh renders, read when the server starts |
| minio | a fixed root user | from a file, read when the server starts |
**In five of seven, a new administrative value takes effect only through a command run with the old
one.** The credential file is mounted directly into both the server and the provisioner. So replacing
it recreates the provisioner, which then holds only the new value while the backend still expects the
old one, and the provisioner is locked out. That is worse than changing nothing.
Every provider module also has its own bus account, an own secret, read at start.
## What the tables show
1. **Every credential provider already rotates in place.** All nine re-apply the password on the
same login each time `create` runs. The controller's `rotate` command relies on that: it replaces
the credential in the inventory and sends both ends in one push. Its own comments state the window,
between the provider applying and the consumer restarting, in which the consumer cannot
authenticate.
2. **Eight of nine name the consumer's resource after its login.** Only gitea separates them,
because an organisation owns the packages. A second login therefore has no resource of its own to
reach, and cannot share the first one's without the adapter granting it.
3. **Five of nine destroy the consumer's data when they remove the login**: postgres, mssql and
mongodb drop the database, lavinmq drops the virtual host with its queued messages, and mailu
deletes the mailbox with its mail. minio drops only an empty bucket, and redis leaves the keys. In
those five, *retire a login* and *delete the consumer's data* are one call. With the harness keyed
by login, a changed login triggers it too.
4. **One backend holds two passwords on one login** (redis). Two hold several tokens beside one
password (gitea and mailu). A rotation built on two secrets per login would work for three
providers out of nine.
5. **Eight of nine can give two logins the same rights over one resource.** Group roles in postgres,
database roles in mssql and mongodb, permissions in lavinmq, a shared role in mosquitto, a shared
policy in minio, a shared key prefix in redis, a shared team in gitea. mailu cannot, because its
user is its mailbox, but it can give one user a second token. So every provider can hold **two
credentials** over one resource, though not every one as two logins. No adapter does either today.
6. **Ownership is a trap in two backends.** In postgres whatever a login creates is that login's, so a
second login cannot alter the first one's tables, and the first cannot be dropped while it owns
them. The one-step way out deletes them. In mssql, a login cannot be dropped with a session open,
nor its user while it owns a schema.
7. **The administrative credentials have one party and a fixed name**, and five backends take them
only at first initialisation. The provisioner needs the old and the new value at once to change
them. Today nothing can give it both.
8. **A consumer's identity is already the resource's name.** The login is derived from the
assignment, which is a module on a node, so the current login and "the consumer" are the same
string today. A second login would need a new name. The resource can keep the one it has.
## Seen on the way
The redis configuration names no ACL file, so a consumer's ACL user exists only in memory. A restart
of the redis server erases every consumer's user. The provisioner does not create them again until it
restarts itself, because its in-memory record says they are done. That is not a rotation finding, but
it is a live fault, and it is recorded here so it is not lost.
@@ -1,46 +0,0 @@
# 02 — The readers
How a secret reaches a running process, and what makes the process take a new one.
## No module watches a secret
A search of every module's code in the catalogue found no file watching of any kind, and no
re-reading of a secret while running. **Every reader reads a secret when it starts.** There is no
consumer that takes a new value live, so every rotation that changes what a consumer presents ends in
the consumer restarting.
## The host already recreates what read a changed file
The node host records, for every long-running container, the digest of each file it read when it
was created: its env-files, and every file bind-mounted into it directly. When a digest changes, the
host recreates the container, even though its spec is otherwise unchanged. This is the fix for
[issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md).
It is on the host's main branch, while the issue is still recorded as located, not fixed.
Two cases are deliberately left out and need `restart-on` in the definition:
- a file read out of a **mounted directory**, because the host cannot know whether the service reads
it once or watches it (a route proxy re-reads its routes live; a provisioner polls what it receives);
- a **process** rather than a container.
## What the catalogue does with it
22 definitions declare a secret they receive. In 16 of them it reaches the service through a
rendered file, usually an env-file. That case the host already covers. 14 declare `restart-on` for
something. Whether each of the 22 is fully covered depends on how its secret travels: through an
env-file or a direct mount, which the host covers, or through a directory or into a process, which
needs `restart-on`. **That was not classified module by module.** It is the check to run before a
rotation mechanism relies on it.
The count covers only the `secrets` field. The 49 modules with their own bus account, and 54 with any
own secret, are readers too, and their bus accounts are rotated like any credential two parties hold.
Their files are mounted directly, which the host covers, but the classification has to name them.
## What this means for rotation
- The *read at start* half of 0113's recipient model is already true, and mostly already handled by
the host. The restart is derived from the files a container reads, not declared per secret.
- Any mechanism, in place or overlapping, ends with the reader being recreated. What differs is
whether the credential it held until then still works.
- For a single-party secret, a module's own, the reader is also the only holder. There is nobody to
overlap with, and delivering the new file recreates the reader.
@@ -1,79 +0,0 @@
# 03 — The options
Three mechanisms, weighed against [01](01-the-providers.md) and [02](02-the-readers.md).
## A. In place, as today
The vault makes a new value. Every applier re-applies it on the same login, which all nine
providers already do. Every reader is recreated by the host.
- **Works with:** every provider, unchanged. It is what `rotate` does now.
- **Costs:** a window per consumer, from the provider applying to the consumer being recreated. They
are on different machines, and nothing orders them. A reader whose machine is unreachable from the
mesh but still reaches its provider stays locked out until the mesh reaches it again.
- **Admin credentials:** the natural form. The provider module is the only party, and it has to
apply the new value with the old one anyway (finding 6).
## B. Two secrets on one login
The applier adds the new password beside the old one, readers move, and the old one is removed.
- **Works with:** redis natively, and gitea and mailu through tokens. **Not** with the other six, whose
backends hold one password per login (finding 4).
- **Verdict:** not a mechanism, a special case. Using it where it exists and something else
elsewhere is the "this way or that way" the design is trying to remove.
## C. Two credentials per consumer, over one resource
The consumer has two credentials and uses one at a time. The applier ensures the other with the new
value and gives it the same rights over the consumer's resource. Readers move to it, and then the old
credential is retired, which removes the credential only, never the resource. **What a credential is,
is the adapter's**: a second login for eight providers (finding 5), a second token on the same login
for mailu. The mesh sees one mechanism.
- **Works with:** every provider, **after** each adapter changes:
- the resource is named after the consumer, not the login. Today the two are the same string (finding
8), so existing resources keep their names, and the current login stays one of the two;
- the resource is owned by the resource, not by a login. In postgres that is a role no one logs in
as, which each login works as, and ownership of an existing database moves to it once (finding 6);
- both credentials get the same rights, over data and structure;
- *retire a credential* and *remove the consumer* become two operations. Today they are one call, and
in five providers that call destroys data (finding 3). The harness must key by consumer, so that a
changed login is not a removal. This is the whole of the danger, and it has to be split, whatever
else is chosen.
- **Costs:**
- every credential adapter changes;
- the harness learns the alternation and a confirmation per step, and rotation state has to live
somewhere that survives a restart, which the harness's memory does not;
- the second login's name must fit the tightest backend. That is 20 characters for a minio access
key ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)), and
a suffix spends part of it;
- retiring an mssql login has to end its sessions first.
- **Gains:** no window. A reader that cannot be reached keeps a working credential until it can.
- **Does not apply to** single-party secrets: admin credentials and a module's own secrets. There is
no second party to overlap with.
## Independent of the choice
- **Split remove, and key the harness by consumer.** Retiring a credential, or a login changing, must
never be able to destroy a consumer's data. That holds under A too, because A's remove is the same
call.
- **Admin credentials are applied by their own provider**, using the old value, with the new one staged
beside it. Five backends take the value only at first initialisation. Replacing the file first locks
the provisioner out (finding 7).
- **Classify the readers** ([02](02-the-readers.md)), bus-account readers included, before relying on
derived restarts.
## Recommendation
- **Two-party credentials, consumer credentials and bus accounts: C**, because it is the only
mechanism every provider supports, and it closes the window instead of shortening it. Its prerequisite, separating the resource from the login and
retiring a login from removing a consumer, is worth doing on its own, because it removes a
data-loss path that exists today.
- **Single-party secrets (admin credentials, a module's own): A, staged.** In place, applied by the
provider that holds them, with the new value beside the old until it has taken.
- **Until the adapters are changed, A stays** as `rotate` implements it, with its window stated. It is
not replaced by a mechanism the providers cannot yet carry.
This is two mechanisms, split by a property of the secret rather than by provider: whether it has one
party or two. Every provider is treated the same way for the same kind of secret.
@@ -1,47 +0,0 @@
---
status: active
initiated: 2026-09-26
touches:
- 00-META/mission.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
- 03-DESIGN/01-to-be/06-the-controller.md
- 03-DESIGN/01-to-be/09-the-node-lifecycle.md
- 03-DESIGN/00-as-is/09-interfaces-and-observability.md
---
# 017 — A mesh that heals itself
**What.** The behaviour the operator wants: a mesh that runs itself. It notices what is wrong,
repairs what it can, and hands what it cannot repair to someone who can, with the reason. This effort
writes that wish down as intended behaviour, designed for the bus the mesh is moving to
([ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md): NATS). It also records what can be done
pragmatically before that move.
**Why.** The mission is *a mesh that controls itself* ([mission](../../00-META/mission.md)). The
mesh can tell whether it is up. It cannot tell whether it is right. The as-is page on observability says so
([as-is 09](../../03-DESIGN/00-as-is/09-interfaces-and-observability.md)). To-be 06 names an
`observability` context in the controller and leaves its store undecided. Nothing routes a condition
the mesh cannot fix to anyone. The cost is measurable: **46 of the 116 issue reports in this
repository describe a failure that was silent.** A mesh that heals itself is, first, a mesh that stops
failing silently.
**What it touches.** The controller's observability context, the node lifecycle's liveness, delivery
([ADR 0010](../../02-DECISIONS/0010-delivery.md), [ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)),
the provisioner harness, and rotation, which is proposed alongside to-be 27 as ADR 0114.
**Documents.**
- [01 — The intended behaviour](01-the-intended-behaviour.md): the wish, as principles and as how
the mesh behaves once the bus is NATS.
- [02 — Now, pragmatically](02-now-pragmatically.md): what is done before NATS, why it does not
build anything the move would throw away, and what has been done already.
**Next.** Two measurements this effort owes before it can graduate:
1. **Every loop in the mesh**: what it converges, and whether it compares against observed state or
against its own memory. Issue 120 found the provisioner harness trusting memory. The same pattern is
expected elsewhere.
2. **The 46 silent failures, classified**: a missing observation, a loop trusting memory, or a missing
escalation. That shows which mechanism removes the most of them.
@@ -1,85 +0,0 @@
# 01 — The intended behaviour
The operator's wish, written as behaviour: what a person or an agent sees the mesh doing. This is a
target to design toward, not a design. Every part of it is to be decided through a record before it
is built.
## Principles
**1. Every loop compares what should be with what is, never with what it did.** Desired state is the
mesh's: assignments, requirements, seats. Observed state is read from the thing itself: the container,
the backend, the node. A loop that compares against its own memory of what it applied is blind to
anything that changed behind its back. That is issue 120, and it is the pattern this whole effort is
written against.
**2. Healing is the ordinary path run again, never a second path.** Repairing a lost login is
provisioning it. Repairing a dead container is converging the node. Repairing a stale declaration is
delivering it. A repair that needs its own code is a second way of doing something, which is exactly
what the mesh is removing everywhere else.
**3. A repair never destroys.** Healing may recreate, re-provision, re-deliver and restart. It may
never delete a consumer's data, retire a credential someone still uses, or pick a winner between two
contradictory states. Where the only repair is destructive, it is escalated.
**4. Nothing fails silently.** Every condition the mesh cannot repair within its budget becomes
visible. It is named, it says since when, why, and who can resolve it. It is visible until it is
resolved, and resolved by observation, not by someone clicking it away.
**5. What the mesh cannot fix goes to an agent.** Per the mission, an agent may be human or not. A
condition that needs judgement is handed to one, as work, with what the mesh knows. It is not handed
over as a notification that someone may or may not read.
**6. Correctness, not only liveness.** A running process that authenticates with a dead credential,
serves an old version, or routes nowhere is not healthy. What a provision's contract promises is what
is checked: the credential authenticates, the route answers, the version is the declared one.
## The loop, everywhere
Every part of the mesh that owns something runs the same loop:
1. **know** what should be true: from assignments, requirements and seats;
2. **observe** what is true: from the thing itself, on its own cadence;
3. **repair** the difference by running the ordinary path again, within a budget of attempts and
time;
4. **raise** a *condition* when the budget is spent or the only repair is destructive;
5. **clear** the condition when observation shows it resolved.
A **condition** is a durable fact about something the mesh owns, such as a node, an assignment, a
provision, a seat or a rotation: what is wrong, since when, the evidence, what was tried, and who can
resolve it. Conditions are the one thing a person or an agent looks at to know whether the mesh is
right. `status` is the list of open conditions. When it is empty, the mesh is right, not just up.
## On NATS
[ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md) moves the bus to NATS, and NATS makes most of
this cheaper, because observation becomes something every component publishes rather than something
a central process polls.
| the wish needs | on NATS |
|---|---|
| every component says it is alive | a heartbeat on a subject per node and assignment; silence past its interval is a condition, and nobody polls |
| every component says what it observed | observations published on subjects (`mesh.observed.<node>.<assignment>`, for instance), consumed by whoever owns the comparison |
| the last known state survives restarts | a JetStream key-value bucket of observed state per owner; the provisioner's "what I applied" and a rotation's step live there, not in memory |
| conditions are durable and watchable | conditions as entries in a key-value bucket, watched by anyone who cares: a surface, an agent, the controller |
| the bus itself is observed | the server's advisories (a consumer exceeding its deliveries, a slow consumer, a client disconnecting) and its monitoring endpoint become observations like any other |
| a repair is retried, not lost | JetStream redelivery with delay, which is the same mechanism [ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)'s guarantee moves to |
| work handed to an agent | a condition that needs judgement published as a task on a subject an agent's queue group consumes |
**Who compares.** Each owner compares its own: the host for its node's containers and files, a
provisioner for its backend, the vault for rotations, the controller for delivery and seats. The
controller's observability context does not repair anything. It holds conditions, their history,
and the view across the mesh. It notices what no owner can see about itself: an owner gone silent.
## What stays human
Some repairs need the operator's key, and the mesh says so rather than pretending otherwise:
re-raising the vault or the broker, and recovering a node's identity. These are conditions too, with
the procedure named, and they are the only ones that can never clear themselves.
## Open
- The budgets: how many attempts, over how long, per kind of repair.
- How a condition that needs judgement reaches an agent, and how the agent's action is recorded.
- Where the observability context stores history (to-be 06 left it open; volume argues against the
relational store).
- Which correctness probe each provision's contract offers, and how often it runs.
@@ -1,46 +0,0 @@
# 02 — Now, pragmatically
The intended behaviour lands on NATS. The bus moves after the migration's core
([ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md)). Until then, work toward it is chosen by one
test:
**Does it survive the move?** A change to what a loop compares against, or to what an adapter can
tell about its backend, survives, because it is independent of the bus. A new AMQP queue for health
reports, a poller written against the broker's management API, or a condition store built on the
current broker does not survive, and is not built.
## Done
**The provisioner asks the backend, not memory** (issue 120). The SDK's harness gained an optional
`holds` on the adapter, asked for every applied consumer every minute. A consumer the backend no
longer holds is provisioned again. Being unable to ask is not treated as loss. The cache module
implements it first, because its server keeps its users in memory and forgets them all on a restart.
That was verified against a real server: a restart erases every consumer's user, and `holds` answers
correctly for absent, present, wrong-password, disabled and deleted.
Changes: mesh-sdk PR #7 (0.1.1) and mesh-catalog PR #84.
This is principle 1 applied to one loop. It survives the move unchanged. On NATS, the harness's
record of what it applied moves from memory into a key-value bucket, and `holds` stays as it is.
## Next, in order of silent failures removed
1. **`holds` for the other credential providers.** Each backend can answer whether a login exists
with the mesh's password without changing anything. Where a backend cannot check a password without
logging in, logging in is the check.
2. **The harness's other blind spot.** A consumer that goes away while its provisioner is down is never
removed. The fix is the same principle in reverse: list what the backend holds, and compare it with
what the mesh asks for. Removal stays subject to ADR 0114's rule that it never follows from a login
changing.
3. **`status` reports what owners already know.** The host knows which containers it recreated and
why. A rotation knows who it waits on. Delivery knows what is outstanding. Surfacing those as
conditions in the existing `status` needs no new transport. It is the shape the NATS condition store
will hold.
4. **The loop inventory and the classification** in [00](00-overview.md). They decide what comes after
these three.
## Not now
- Heartbeats, observation subjects, key-value state, advisories: all NATS, all after the move.
- Handing conditions to agents: designed with NATS, where a task on a subject is native.
- Choosing the observability store: decided when there is something to store, which is after the
move.
@@ -1,80 +0,0 @@
---
status: active
initiated: 2026-10-02
touches:
- 02-DECISIONS/0040-what-a-module-is.md
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 03-DESIGN/01-to-be/05-the-node-host.md
- 03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md
- 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
- 03-DESIGN/01-to-be/34-the-console.md
- 03-DESIGN/00-as-is/10-module-catalogue.md
- 04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
became: []
---
# 018 — The operator's machine as modules
**What.** The mesh owns the whole machine, not only the services on it. Everything a person
configures on a node — the login manager, the display server, the window manager, the shell, the
terminal, the launcher, the notifier, the audio setup, the boot images, the downloads folder, the
agent at the terminal — is a module: a package, the files it owns under `/etc` and under the
operator's home, the seat it holds, the tools it serves. One default configuration per module,
varied per node only through settings rendered into the file or a kept operator region, never
through an edit. The servers take the universal modules (shell, prompt, git, the agent); the
workstations take those and the graphical stack, which a capability the machine reports gates.
This effort writes that behaviour down, measures what the predecessor's desktop modules actually
contain, and settles what the mesh must gain before the first of them can be written.
**Why.** The predecessor is retired on every node. What it still owned on the two workstations —
about thirty modules' worth of dotfiles, user units and `/etc` files — is now owned by nothing:
no generator regenerates them, and a fix to one of them is a hand edit that nothing records. The
migration scoped these modules out as *the workstation's own environment*, and
[to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) names them as the last
thing the predecessor was keeping alive. To-be 29 covers one directory, `~/.ssh`, and draws a
boundary inside it. The operator wants no boundary: the machine is the mesh's, as far as it makes
sense to configure it. That is a wider scope than any design states, and it reaches three records
that were written for services: what a module is, where a module's tools run, and what a managed
file may be.
**What it touches.** The module definition ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)),
seats and their contracts ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
where a module's tools run ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6), the host's vocabulary
([to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md)), managed files and settings
([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md),
[issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)),
and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalogue.md)).
**Documents.**
- [01 — The intended behaviour](01-the-intended-behaviour.md): the operator's wish, written as
how the mesh behaves, in the mesh's own words.
- [02 — What exists, and what is missing](02-what-exists-and-what-is-missing.md): the
predecessor's desktop modules measured; which records already say what is wanted; the gaps.
- [03 — One tool executor per node](03-one-tool-executor-per-node.md): where a module's tools
run. The direction the operator set, the evidence for it, and what it supersedes.
- [04 — The seats of the environment](04-the-seats-of-the-environment.md): the roles a machine
has once, their candidate contracts, and what gates each.
**What this must settle before it graduates.**
1. A module is one *managed thing*, software or not, and every module may serve tools — or ADR
0040 already says this and only its examples are narrow.
2. One tool executor per node, host-side, module-agnostic; which records it supersedes and
in what form the console continues.
3. Per-node variation is a setting rendered into the file or a kept region, never an edit —
ADR 0011 stands — and issue 168 is fixed before any environment module carries a setting.
4. User-scoped units on the host's `service` shape, and a service-manager seat whose holder
serves the tools about them.
5. The operator account stated on every node; today no node record carries one.
6. The seats of the environment and their verbs, one record per seat, slowly, because a
seat's tools bind every future holder.
7. Where the environment modules live: this catalogue, or one of their own as the media chain
has; and whether a third-party organisation's tooling belongs in a public catalogue at all.
@@ -1,99 +0,0 @@
# 01 — The intended behaviour
*Written 2026-10-02 from the operator's words, in the mesh's words. What is wanted, before what
exists. Where a sentence restates a record, the record is named; where it goes further, that is
said.*
## The machine is the mesh's
**Everything configurable on a node is declared by a module.** Not only the services the mesh
runs: the login manager, the display server, the window manager, the bar, the launcher, the
notifier, the compositor, the lock screen, the terminal emulator, the clipboard, the shell and its
prompt, the editor, the audio setup, the boot images, the package manager's configuration, the
agent a person runs at a terminal, and the folders a person works in — a downloads folder that is
tidied, backed up, distributed to other nodes and asked questions of. System folders and the
operator's home alike. The operator is the only person on every node, so the mesh manages the
person's machine, not a machine with a person on it.
This is [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)'s definition applied without
the service bias its examples carry. A module is one managed thing, named once, described
completely by its manifest. It may have a package, files, a container, a unit, a binary, a seat it
holds, and tools it serves — any one of these, or all, or two. There is **no kind of module**: zsh
has a package, files, a seat claim and the tools that claim obliges it to serve; downloads has a
folder, a process and tools; nftables has a package, files, a service, a seat and tools. The
difference is what each declares, not what each is.
**The home has no boundary.** [To-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
owns one directory under the home and draws a line inside it between the mesh's and the person's.
Here the line is drawn only by what the modules declare: every file some module places is the
mesh's; what no module declares is found and left alone, exactly as the adoption rules already
say for a machine. The reach is bounded by sense, not by a rule — the mesh configures what can
be configured, and a person's documents, projects and history are data under
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), not configuration.
**A module names no node and no path.** The operator account is a node fact and the home is
derived from it ([to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2,
shipped in the controller; its record is proposed in an open change). A module places a file
*under the home, owned by the account*, and the same manifest lands on a server and a laptop.
## One default, varied by settings, never by edits
**One module, one default configuration.** The window manager module ships the configuration
that is right for every node. There are no flavors: the predecessor's one desktop module carried
four, one per class of machine, and what differed between them is what settings are for.
**A node varies a module in exactly two ways.** A **setting**, declared by the module with its
type, meaning and default (proposed alongside the container-runtime records), set for the mesh
or for one node, and rendered into the file at composition — the value is in the file, not in an
environment variable the file reads. Or a **kept region**: a block in a file the mesh writes
*into*, where the operator's own lines survive every push
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). An
edit to a managed file outside such a region is not a third way; it is overwritten, as
[ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) says, and the
predecessor's habit of adopting disk drift back into its database is not carried over.
The predecessor's theming — some ninety environment variables substituted into templates at sync
time, with tools to list and set them — is the same idea with the wrong rendering. The knobs
become declared settings; the file carries the value.
## Roles a machine has once are seats, and seats carry tools
**A role a machine fills at most once is a node-scoped seat**, declared by a module
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): the login shell, the
display session, the display server, the terminal emulator, the launcher, the notifier, the
compositor, the lock screen, the service manager, the boot loader. Several modules may be able to
hold one — zsh, fish and bash can all hold the login shell — and the assignment on each node says
which does. Installing a shell is installing software; holding the seat is being *the* shell.
**A seat's contract is its tools** ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
Every holder of the login-shell seat serves `execute`, which takes one string, the command, and
runs it on the node the seat is scoped to. Every holder of the boot seat serves "rebuild the boot
images", so *"rebuild your boot images"* is a verb addressed to a machine, not a one-off step in
a hook. Every holder of the service-manager seat answers for the units on the machine, system and
user scope. A module may serve its own tools beside the seat's
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §2): show the rendered
configuration, set a theme value, report status.
**Any tool may be called from any node.** The operator's statement, and the grant model it
implies: the executor on each node may call everything, as the console already may. A verb that
needs root on the machine is the module's concern — the tool escalates, the executor and the
caller do not know.
## Servers and workstations differ by capability, not by catalogue
The same catalogue serves every node. A module declares what it needs — a graphical session, a
display server, a container runtime — and the machine reports what it has, as the profile already
reports eight capabilities today ([issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)).
Assignment refuses the wrong placement by name
([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3). So every node takes the shell,
the prompt, git and the agent; only a node with a graphical session can take the display server,
and only a node holding the display server can take a window manager. Nothing in a module says
"workstation".
## What the operator would say to the mesh
*Set the login shell on the build node to fish. Rebuild the laptop's boot images. Show me the
window manager's effective configuration on the desktop and where each value comes from. Give
the downloads folder on the laptop to the home server. Run `uptime` on every node.* Each of these
is a seat verb or a module tool, addressed to a node, answered by whatever holds the role there.
@@ -1,91 +0,0 @@
# 02 — What exists, and what is missing
*Measured 2026-10-02 on one installation: two workstations, two servers, all four converged to
the mesh; the predecessor retired on the last workstation the day before. Numbers are from the
machines and the repositories, not from memory.*
## 1. What the predecessor's desktop looks like
The predecessor's catalogue on the laptop held **34 modules**, of which **28** are the operator's
environment rather than services. By what they declare:
| shape | count | examples |
|---|---|---|
| package only | 9 | browser, mail client, process monitor, media player, file manager, chat |
| package + `/etc` files + system service | 5 | login manager, display server, power and thermal daemons, package manager configuration |
| package + files under the home | 6 | shell and prompt, the agent at the terminal, scripts, the sync client, a music player |
| files under the home + user units + hooks | 2 | the desktop environment, audio |
| third-party organisation tooling | 6 | out of scope here |
**The desktop module alone** declares **88 files**, **4 flavors** (the window-manager stack, and
one per class of machine), **2 user units** with a hook to enable them, 8 files under `/etc`, a
wallpaper shipped as an asset, and reads **about 90 environment variables** as theme knobs,
substituted into its templates at sync time and set through a theming tool. Its hook exists
because *shipping a unit file does not run it*: one unit had been deployed for months and ran on
one machine only, because somebody had enabled it there by hand.
**The shell module** ships `~/.zshrc`, the prompt configuration, an `~/.ssh/config` that the
predecessor generated from its registry, and a `LOGIN_SHELL` variable applied with `chsh` by a
hook. Two flavors: the prompt theme, and autocompletion.
**Other modules write into the desktop module's files.** The chat client places i3 and notifier
snippets into `config.d` directories the desktop module owns, and its launch flags, window
placement and notification colours are each a variable with a default.
**One-off steps live in hooks** across the set: enable user units, `chsh`, create a swap file,
`mkinitcpio`, enable a vendor VPN service the package ships disabled. Every one is state the
host could declare or a verb a seat could serve; none is today.
## 2. What the migration did with them
The migration's module to-do scoped the whole set out as *desktop / workstation ricing — the
workstation's own environment* and *node/OS tooling — managed on the node, never catalogue*. The
last workstation's runbook then split the same set three ways: **A**, system scope, which the
host's vocabulary can express today (the login manager, the display server, the power daemons,
the package manager, the container runtime); **B**, under a home or a user unit, waiting on
to-be 29; **C**, package only, the operator's call. The migration log closes the workstation with
*the operator's desktop awaiting its design*.
Two things followed from scoping them out. Nothing regenerates those files now, so a fix is a hand
edit — the login manager's session script was fixed this way on the day of writing, and recorded
in a repository nothing deploys from. And the one piece of this family written as a mesh module,
the ssh client, was closed on hold in the catalogue until the controller carried the account fact.
## 3. What the records already give
| wanted | record | state |
|---|---|---|
| one module per managed thing; every module may have tools | [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) | accepted; examples are services, and the shell is named as a *shared* seat |
| a module declares its own node-scoped seat | [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) | accepted |
| a seat's contract is its tools; a holder may add its own | [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) | accepted; one node seat serves verbs live |
| a capability the machine reports gates a holder | [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3 | accepted; the profile already reports `graphical-session` |
| the account as a node fact; a file under the home owned by it | [to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2 | built in the controller; its record proposed in an open change |
| inside a home: owned, written into, written by the module, found | proposed in the same change | proposed |
| a setting declared with type, meaning, default and cost | proposed with the container-runtime records | proposed |
| a managed file is derived; an edit is overwritten | [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) | accepted |
| the mesh writes into a shared file, never over it | [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md) | accepted |
| a module names no path; the host resolves the home | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) | accepted |
| the `user` shape: a login shell is declared state | [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md) | designed; used by no module |
## 4. What is missing
1. **The account is recorded nowhere.** The node record has the column; on all four nodes it
is empty. Every home-scoped module is unassignable until the operator states it.
2. **User-scoped units.** The host's `service` shape has no user scope. To-be 29 says it
plainly: *a workstation's per-user daemons have no form the mesh can send.* The desktop
module's two units, the audio masks, the power module's memory guard and the thermal
daemon's profile switcher all need it.
3. **One-off steps.** `mkinitcpio`, `chsh`, creating a swap file. Each is either declared
state the host lacks a shape for, or a verb a seat should serve. An action in a declaration
is refused over the link, and rightly.
4. **Settings leak** ([issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)):
a setting reaches every mergeable file and every contribution of its module. Ninety theme
knobs on that mechanism would reach ninety files. The proposed settings record says a setting
names the file it lands in; that has to ship first.
5. **Where tools run.** Every module that serves a tool today does so from its own container
per node. See [03](03-one-tool-executor-per-node.md).
6. **A seat's verbs are undecided for every seat but three.** To-be 33 leaves which verbs each
seat serves as *a decision per seat, slowly*. The environment adds a dozen seats.
7. **Catalogue placement.** The media chain left this catalogue for its own; whether the
environment does the same, and whether a third-party organisation's tooling belongs in a
public catalogue, are unasked.
@@ -1,88 +0,0 @@
# 03 — One tool executor per node
*The direction the operator set on 2026-10-02, the evidence it rests on, and what it supersedes.
A direction, not yet a decision: the record is written when this effort graduates.*
## Where tools are served today
[To-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) names three families — a
role's tools on the seat, a module's own tools on the module, the mesh's own verbs on the
controller seat — and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
says a runtime serves the subjects its membership issues. What *runs* that runtime is
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
one supervised process per module, under the module's own account, carrying that module's
compiled tools. Measured on the live mesh:
| who answers | how it runs | count |
|---|---|---|
| the mesh's own verbs | the controller binary, on its node | 17 verbs |
| the store seat | the store's own runtime | 2 verbs |
| the packet-filter seat | **a container per node**, built on the tool-runtime base image, with the network namespace and `NET_ADMIN`, on all four nodes | 3 verbs and 1 own tool |
| every module's own tools | the module's container, one per node it runs on | 67 tools across the catalogue |
| the console | a container per node, loopback MCP, `invokes: *` | serves none, calls all |
| the host | — | serves nothing; answers no question about the machine |
**The packet-filter holder is the case to look at.** The module is a package, three files and a
system service. To serve three verbs it also declares a built image and a container on every
node whose only job is to answer them. Scaled to the environment — a shell, a prompt, a launcher,
a notifier, a compositor, a login manager, a service manager, a boot loader, a downloads folder —
that is one container per module per node for software that is itself not a container, and the
operator's judgement is that tools should not run inside a container at all.
## The direction
**One tool executor per node, on the host side.** A process the host supervises, the way the
launcher supervises the host ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): not a
container, one bus credential for the node, module-agnostic. It loads the tool code of every
module assigned to the node and serves each module's tools and each held seat's verbs on the
subjects the membership issues — nothing changes in what [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
say about subjects, grants and memberships; what changes is that one process subscribes for the
node instead of one per module.
- **A module brings its tools as a built artifact**, a bundle the pipeline produces, never an
image. The executor knows bundles and subjects; it knows nothing of zsh or nftables.
- **A tool is code the module wrote**, one function behind an MCP verb. `execute` on the shell
seat is a function with a string argument. The executor does not declare, template or
interpret tools; it runs them.
- **Root is the module's concern.** A tool that must change the packet filter or rebuild boot
images escalates itself. The executor does not run as root for everyone, and the caller does
not know.
- **Any node may call any tool on any node.** The executor's credential may call everything,
as the console's already does; per-module grants on the calling side are not kept.
- **The mesh's own verbs stay with the controller** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
and a mesh-scoped seat's verbs run on the node that holds it
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
No hub is added; the controller's node is already one.
**The console is the executor, renamed.** It already runs on every node with a credential that
may call everything, and it already serves the mesh's tools to whoever is on the machine over
MCP on loopback ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
It moves out of its container into the host's process tree, gains the serving half, and takes a
name that says what it is — *the node's tool runtime* or simply *node tools*; "console" names
the operator's half only.
## What it supersedes, and what it keeps
| record | effect |
|---|---|
| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) | superseded *for tools*: one process per node runs every module's tool code, under one account. A module's long-running service — a daemon, a container — is untouched; the executor runs tools, not services. The record must say why one account for every module's tools is acceptable: every tool may be called from every node anyway, and root is taken by the tool, not granted to the process |
| [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) | kept in substance — a module, assigned per node, loopback MCP, the machine's login is the authority — changed in form: host-side, not a container; serves as well as calls; renamed |
| [to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6, [to-be 34](../../03-DESIGN/01-to-be/34-the-console.md) | amended the same way |
| [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §3, *a container may ask for a capability* | moot for that holder: the verbs run on the host side and escalate as they need |
| the container-runtime seat, proposed in an open change: *the holder runs as a supervised process and serves the verbs locally to the host and on the bus* | consistent — a supervised process serving verbs is what the executor is; the open question is whether that holder keeps its own process or serves through the executor like everyone else |
| the tool-runtime base image | no longer the way tools reach a node; may remain the way a module's *service* is built |
## What stays open
- **The executor's language.** The host is a static Go binary and loads no plugins, so the
executor is a sibling process, and its language decides the language of every tool bundle.
One decision, taken once.
- **How a bundle reaches the node.** An artifact of the module's build, delivered as the host
delivers everything else; whether it is a file resource in the declaration or a thing the
executor fetches by digest.
- **Reload.** A push that adds or upgrades a module's bundle reaches a running executor as a
reload, not a restart, or every tool on the node blinks on every push.
- **The host's own questions.** [Issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)
wants a machine to say more about itself. With an executor on every node, "what is this
machine made of" is a seat verb like any other, served there.
@@ -1,65 +0,0 @@
# 04 — The seats of the environment
*Candidates, not decisions. To-be 33 says which verbs a seat serves is a decision per seat,
taken slowly, because a seat's tools bind every future holder. This document lists the roles the
operator's machine has once, who could hold each, what gates it, and a first verb or two — so
each record has a starting point.*
## The rule for what is a seat here
A role the machine fills **at most once** is a node-scoped seat, declared by the module family
that fills it ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A thing
several of which coexist without contention — editors, browsers, media players — is not a seat;
each is a module with its own tools, and nothing is singular about it. A seat is held by one
assignment per node; other modules of the same family may be installed beside it without
holding it ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) §1, read with the sharper
distinction: *installed* is not *holding*).
## Candidate seats
| seat | holders | gated by | first verbs |
|---|---|---|---|
| **login shell** | zsh, fish, bash | nothing: universal | `execute(command)`; `show-config`; the holding itself sets the account's login shell through the host's `user` shape |
| **service manager** | systemd | the `service-manager` capability the profile reports | units: list, status, start, stop, restart, enable, journal; **user scope** on each |
| **boot** | grub, systemd-boot | a machine that boots itself (not a container host) | `rebuild-images`; `entries` |
| **package manager** | pacman, apt | the `package-manager` capability | search, installed, upgrade, orphans; today a capability the host uses, not a seat anyone holds |
| **display server** | xorg, wayland compositors that are their own server | the `graphical-session` capability | `displays`; `layout` |
| **display session** | i3, sway | the display server seat held on the node; i3 needs x11, sway needs wayland | `reload`; `workspaces`; `windows`; `move` |
| **terminal emulator** | xterm, alacritty, foot | display session | `open`; `font` |
| **launcher** | rofi, dmenu | display session | `show`; `theme` |
| **notifier** | dunst, mako | display session | `send`; `history`; `rule` |
| **compositor** | picom | display server (x11 only) | `restart`; `effects` |
| **lock screen** | i3lock, swaylock | display session | `lock` |
| **bar** | i3status-rust, waybar | display session | `reload`; `blocks` |
| **login manager** | lemurs, greetd | graphical session | `sessions`; `default-session` |
| **audio** | pipewire, pulseaudio | the machine reports a sound device | `sinks`, `sources`, `default`, `volume`, `mute` |
| **clipboard** | greenclip, cliphist | display session | `history`; `clear` |
Not seats, modules with their own tools: the editor, the browser, the mail client, the file
manager, the media player, the chat client, the agent at the terminal, the downloads folder, the
scripts folder, the sync client, the power and thermal daemons that are specific to one machine's
hardware.
## What the table implies
**Capabilities come first.** `graphical-session`, `service-manager` and `package-manager` are
reported today. *A display server is held* is not a capability but a seat being held, and a
module that needs it declares a dependency on the seat, not on a capability: *i3 needs the
display server seat held by xorg*. Whether a held seat can gate another's assignment is a
question for the controller's resolver, and the first environment module after the shell will
ask it.
**The service manager comes early.** Four of the predecessor's modules ship user units, and the
executor itself is a unit. User scope on the host's `service` shape is a host change whichever
module holds the seat; the seat's holder answers the questions about units, it does not apply
them — the host does, as it does for every declared resource.
**The shell comes first.** Universal, no capability, one verb that is immediately useful on
every node, and the `user` shape already makes the login shell declared state. It is the module
that proves the pattern: a package, files under the home owned by the account, a seat claim,
tools served by the executor, settings for the few things that vary per node, and a kept region
for the operator's own lines.
**The login manager is the first system-scope one**, because it needs nothing new: a package,
two files under `/etc`, a service — the same shape the ssh daemon module has today — and the
session script it owns is the file that was hand-fixed the day this effort opened.
+10 -44
View File
@@ -1,46 +1,12 @@
# 01-RESEARCH
Investigations that have not yet hardened into design.
## Structure
Each effort lives in `NNN-descriptive-name/` and **must** contain `00-overview.md`, carrying its
state in YAML frontmatter and a prose summary below it:
```yaml
---
status: active | graduated | abandoned
initiated: YYYY-MM-DD
touches: [] # design docs, subsystems or areas the effort bears on
became: [] # required when status is terminal — what it turned into
---
```
The prose says what is being investigated, why, and what it touches. It does not restate the
status — status lives in one place, and two places is one too many.
Further documents in the same folder hold the work itself: notes, evidence, option analyses,
draft designs.
## Lifecycle
| status | Meaning |
|---|---|
| `active` | Investigation in progress. |
| `graduated` | Checked against `00-META`, decided in `02-DECISIONS/`, and specified in `03-DESIGN` — see `became:`. |
| `abandoned` | Stopped or superseded. Nothing is deleted. |
An effort graduates by producing a decision record **and** a `03-DESIGN` entry. It is abandoned
in place — never deleted. What was rejected, and why, is the more expensive half to
rediscover.
Starting and closing efforts is playbook territory:
[`00-META/process/01-research.md`](../00-META/process/01-research.md) and
[`02-graduation.md`](../00-META/process/02-graduation.md).
## Rules
- Markdown only. Do not skip or reuse a sequence number.
- **Evidence, not assertion.** An effort that measured nothing has not finished.
- Research describes real observations but **never identifies the mesh it observed**. The shape
of a finding survives anonymisation intact.
**The research efforts live in the knowledge base, not here** (ADR 0290). The knowledge base is the module
`mesh-knowledge`, holding the seat `knowledge-base`; it holds every effort whole — its number, title, status,
the day it began, its documents, its links and every version — and it is the only place an effort is read.
This repository keeps no copy. From any node and any folder, through the mesh MCP server:
`knowledge-base.ask` with a question in plain words, or `knowledge-base.get` with `kind: research` and a
number (`reasoning: true`, or `section` with a document's name, for its documents). An effort is started
with `knowledge-base.reserve` and `knowledge-base.propose`, as playbook
[01](../00-META/process/01-research.md) says, and closed through playbook
[02](../00-META/process/02-graduation.md). The efforts left this folder at hq commit 76b346af,
the commit the knowledge base's records name as their source (issue 426).
@@ -1,235 +0,0 @@
---
topic: the mesh
status: accepted
date: 2026-08-22
deciders: jochen
reconstructed: false
---
# 1. The mesh brokers capabilities; nodes host; agents think
## Context
HAL has 124 modules. The count is not the problem — it is the symptom. Modules are split
because splitting is the only granularity the platform offers, and domains are merged
because a shared database is the only integration it offers. Both pressures push in the
same direction: boundaries end up drawn by deployment accident rather than by domain.
Three observations establish the state.
**The word "agent" means two different things.** The original design treated a node as an
agent with thinking abilities. Later, noxflow implemented agents as employees with
skills, workspaces and tasks. Both survive. The collision is visible in the data — there
are **two agent rows per node**:
| agent | skills |
|---|---|
| one named after the node | `{deploy,verify,operate}` |
| one named `hal-<node>` | `{}` |
One carries the work; the other carries only identity, existing to hold a licence for
HAL's own sessions. The same split appears in the schema: `nodes.hal_claude_account` and
`agents.claude_account` are one fact in two tables in two databases, and what was
documented as "three licence touchpoints" is one concept modelled three times.
**Domains integrate by sharing a schema.** `noxflow` is 45 tables spanning five domains —
tasks (7), agents (13), knowledge (8), meetings (7), scheduling (2). Its original purpose
is 7 of 45. Because agent identity lives in noxflow's database, work that belongs
elsewhere must be implemented there: per-agent Claude credentials had to be written by
the noxflow runtime, even though node identity — the same kind of fact — lives in the
mesh registry.
**The core domain has no context to live in.** The mesh's distinguishing feature is that
a module declares `requires: postgres/database` and never learns where the database
lives, who owns the credential, or how it rotates. That is capability brokering, and it
is what makes this a mesh rather than four machines with a configuration manager. Yet the
logic implementing it sits in `modules/postgres/tools/index.ts` — a provider module,
where "the mesh brokers credentials" cannot be expressed. On 2026-08-22 three of its
invariants were found violated simultaneously (see Consequences).
## Considered Options
1. **Keep the current layout; fix bugs as they surface.** Rejected. The faults are not
independent. Every incident on 2026-08-22 — credentials written by the wrong module, a
dependency question answered wrongly twice, an invariant enforced nowhere — traced to
a boundary that was never stated. Fixing them individually leaves the generator intact.
2. **Merge aggressively into few large modules.** Fewer names, same problem: a shared
schema across domains is what produced noxflow, and doing it deliberately would
produce it again at larger scale.
3. **Decompose by bounded context, with the mesh as a broker.** Name contexts after their
aggregates, integrate through a published record rather than a shared schema, and let
deployment granularity be a feature-level concern rather than a reason to create a
module. **Adopted.**
## Decision
### The domain, in one sentence
**The mesh brokers capabilities. Nodes are places where work runs. Agents are personas
that think and act.** Everything else supports one of those three.
### What this is, plainly — and what "mesh" does not mean
*Written 2026-08-29, from working through connectivity and asking whether the word still fits.*
Four layers. Naming them honestly is worth more than the word on the tin:
| | |
|---|---|
| **machines are linked by a private network** | and every machine reaches every other over it |
| **one node holds knowledge of all of them** | the control plane, and only it |
| **modules are how anything is built and delivered** | this *is* the CI/CD, not something beside it ([ADR 0010](0010-delivery.md)) |
| **every node runs a session you can message** | a feature of the node, remembering across callers; any node can message any other ([ADR 0004](0004-a-node-and-how-it-joins.md)) |
| **workers are hired onto nodes to do tasks** | employees, with a lifecycle — a different thing from the row above ([ADR 0003](0003-agents-are-persistent-employees.md)) |
**The last two rows are not the same thing and the vocabulary of one does not describe the other.**
A node's session comes with the machine: nobody hires it, it holds no tasks, it is never
reassigned, and it goes when the node leaves. A worker is an employee — named, hired, drained,
retired, movable. They are built from the same parts and run on entirely different terms, and
collapsing them is how the employee vocabulary ends up stretched over something it does not fit.
**Neither makes the node itself a thinking thing.** A node is a machine; both of these run *on*
one, which is why *a node does not authenticate to a model provider, agents do* is unaffected by
either.
**Where the value is** is that both can reach across the whole set: a shell, a service, a file, or
simply a question to another node. Not machines that can be configured centrally, which is
ordinary, but a set of machines that can be worked across as though they were one.
**This is not a mesh in the peer-to-peer sense and will not become one.** The word describes what
machines can reach, not how they are governed:
| | a mesh? |
|---|---|
| what a machine can reach | **yes** — genuinely any to any |
| how the traffic travels | no — anything crossing sites transits the hub |
| who decides | no. One node, declared |
**And *master* overstates it in the other direction.** A master implies the others need it in
order to function. They do not: every node holds what it was last told and runs from that copy
*always* — not as a fallback, as the only mode it has. So the control plane being gone is every
node in the ordinary disconnected situation at once, and **what is lost is change, not
operation.**
The accurate phrase is **one authority, no failover**, and both halves are deliberate
([ADR 0006](0006-the-substrate-and-the-control-plane.md)).
### Nodes and agents are decoupled
A node is a place where an agent can run — that is the entire relationship. There is no
resident agent, no node-owned identity, no ownership in either direction. Agents named
after a node remain, as **ordinary agents** that happen to hold infra skills.
Consequently `nodes.node_license` and `nodes.hal_claude_account` cease to exist: a node
does not authenticate to a model provider, agents do. The two agent rows per node merge.
### There is one kind of participant, and some are human
Human and non-human participants are both **agents**. Both hold identity and credentials;
both act, remember and coordinate. What differs is **modality** — how an agent acts:
| modality | credential is delivered to |
|---|---|
| spawned session | that agent's own config directory |
| shell or desktop | that agent's home on the node it acts from |
A node holds no licence. **An agent holds credentials, and delivery follows that agent's
node bindings and modality.** A human agent's grant arrives in the home directory of the
user it acts as, on the nodes it is bound to — the same rule that puts a spawned agent's
grant in its config directory, with a different target.
This requires one fact the mesh does not record today: which user, on which node, a given
human agent acts as. Adding it is what removes the node licence — the node is currently
standing in for an identity the mesh cannot name. It is also what makes a second human
agent require no new mechanism.
### Contexts
| context | aggregate | subdomain |
|---|---|---|
| `hal/mesh` | **Provision**, Node, Module — brokering and its bookkeeping | core |
| `hal/agents` | **Agent** — identity, licence, runs, memory, thoughts | core |
| `hal/work` | **Task** — workflows, bindings | core |
| `hal/stream` | **Thread** — mentions, messages, meetings, notifications | core |
| `hal/delivery` | **Pipeline** — jobs, artifacts, features | supporting |
| `hal/knowledge` | **Document** — spaces, revisions, review | supporting |
| `hal/ai` | **Licence** — provider grants and rotation | supporting |
| `hal/observability` | **Check** | supporting |
| `hal/config` | **Setting** — env, secrets, PKI | generic |
A *brain* — memory, thoughts, cognition — is a concept the Agent aggregate owns. It is
not a module. Anatomy makes attractive names and poor boundaries; today `hal/brain` names
infrastructure and `hal/cortex` describes itself as messaging while running nowhere.
### Provisioning is the core domain, not plumbing
`hal/mesh` is a broker; the registry is its bookkeeping. Its invariants are explicit and
owned:
- one rotation source per resource
- a credential change fans out to every consumer
- a consumer never holds a credential the provider does not know about
### Contexts integrate through the record, never a shared schema
`hal/stream` is the published language. A context publishes; it does not join across a
boundary. This is what dissolves "meetings" as a domain — a meeting is a thread, and a
notification is a mention not yet read.
### Third-party software leaves the repository
`plex`, `sonarr`, `postgres`, `verdaccio`, `docker-registry` and 88 others run **on** the
mesh; they are not **of** it. The pipeline and provisioning are deliberately
module-agnostic, so HAL's own modules dogfood exactly what external modules use — which
is what makes the separation safe rather than merely tidy.
## Consequences
**The invariants now have an owner, and were measurably unowned before.** On 2026-08-22,
`provision_ensure` — documented as "NEVER rotates an existing secret" — was found to mint
a new password on every adoption and update only the provider's row. `hal_notifications`
consumers on three nodes held dead credentials for two days; `hal_transcripts` had two
rows written 216 ms apart, so at most one could match the live role.
**noxflow dissolves.** `hal/work` inherits tasks and workflows — the concept it was built
for. Agents, knowledge, meetings and scheduling return to their contexts. The name goes
away.
**`hal/sdk` shrinks.** 155 files, 34,636 lines, containing code from every context —
including `workflow-engine.ts` and `task-commands.ts`, work-domain logic in the kernel
every module imports. Each landed there to avoid a cycle between modules that both needed
it; a domain module can only own its shared code once the domain has a module. Extraction
is therefore downstream of this decision, not independent of it.
**Two mechanisms are prerequisites, not follow-ups.**
- *Named features with per-node opt-in.* Without it, every independently deployable unit
inside a context becomes a module again and the count returns. `hal/claude-licences`
exists solely because one daemon must run on one node.
- *A local mesh in containers.* `dev_up` starts providers "via systemctl (same as
production)" — it borrows the host, and no mesh can be stood up locally. Everything that
manifests **between** nodes is therefore discoverable only in production, which is where
every fault of 2026-08-22 was found. A refactor of this size is otherwise unverifiable.
**Credential delivery becomes uniform.** Per-agent credential directories, built for
spawned sessions, extend to human agents unchanged. A file previously scoped to a node
becomes scoped to an agent — which would have prevented the class of failure where a
rotation reached one node of four while the mesh reported success.
**Migration is incremental and long.** Contexts can be extracted one at a time behind the
existing pipeline. Nothing here requires a flag day, and nothing here is cheap.
## References
- [`01-RESEARCH/001-module-domain-decomposition`](../01-RESEARCH/001-module-domain-decomposition/analysis.md)
— current-state evidence, table counts, open questions
- [`00-META/how-we-build.md`](../00-META/how-we-build.md) — naming and integration rules
- `modules/hal/sdk/src/feature-handlers/index.ts` — `FEATURE_HANDLERS`, the fixed handler
array that makes a feature a singleton per module
- `modules/postgres/tools/index.ts` — the adoption path that rotates a shared credential
- Mediahuis `papa-hq`, ADR 0020 *Composable, independently-shippable modules* — the
constraints that make a unit independently shippable, applicable unchanged to features
- impire.io / soulstream — *the record* as integration substrate, personas over services,
and "cheap awareness and expensive thinking"
@@ -1,69 +0,0 @@
---
topic: the mesh
status: accepted
date: 2026-02-25
deciders: jochen
reconstructed: true
---
# 2. Nodes communicate over a message broker, not over HTTP
> Reconstructed after the fact from the evidence cited below. The decision was taken in
> implementation, not in a record; this document states what was decided and why, not a
> deliberation that happened.
## Context
The mesh is a set of machines that must call each other's capabilities. On the day the
repository was founded there was no inter-node transport at all — each node was configured
independently and shared nothing at runtime.
Three properties were required and are visible in everything built since:
- A node behind a household NAT must participate fully. It can dial out; nothing can dial in.
- A node that is asleep, rebooting or upgrading must not cause a caller to fail — the request
should wait, not error.
- Adding a node must not require editing anything on the nodes that already exist.
## Considered options
1. **HTTP APIs between nodes.** Rejected. Every node becomes a server that every other node
must be able to reach, which the NAT case makes impossible without inbound tunnels to each
participant. It also makes node liveness a caller's problem: a request to a sleeping node
is an error rather than a wait.
2. **Polling a shared database.** Rejected. Latency is the poll interval, load is constant and
independent of demand, and request/reply has to be built on top of it by hand.
3. **A central message broker with per-node exchanges.** Chosen.
## Decision
All inter-node communication goes through a message broker. Every node owns a topic exchange
named for itself and a request queue; a shared mesh exchange carries commands and events that
are not addressed to one node.
Three message shapes, and only three:
- **RPC** — request/reply, for calling a capability that lives on another node.
- **Commands** — instructions to do a stage of work, addressed by what is to be done.
- **Events** — statements that something happened, addressed to nobody.
Every node dials the broker outbound. Nothing dials a node.
## Consequences
- NAT stops being an architectural concern. A node's reachability is a property of the broker
connection, not of its network position.
- A call to a node that is down waits in that node's queue instead of failing. This is usually
right and occasionally the wrong thing entirely — a queued command for a node that never
returns is a stall with no error, which is the failure shape this mesh keeps rediscovering.
- The broker is a single point of failure and a single point of trust. Its credential is
mesh-wide, so rotating it is a mesh-wide operation.
- Tools never leave the host: a remote call proxies over the broker and the credentials stay
where the capability is.
## References
- The broker was stood up on 2026-02-25, the second day of the repository.
- Knowledge base: `mesh` (transport, exchanges, queue naming), `troubleshooting/amqp-credential-rotation`.
- The stall shape is recorded in `troubleshooting/empty-pipeline-blocks-the-queue` and
`troubleshooting/daemon-and-tool-server-share-a-request-queue`.
@@ -1,75 +0,0 @@
---
topic: the mesh
status: accepted
date: 2026-07-12
deciders: jochen
reconstructed: true
---
# 3. An agent is a persistent employee, not an instance of a pool
> Reconstructed after the fact from the evidence cited below.
## Context
Agents were originally a **pool**: a named kind of worker, scaled to some number of
interchangeable instances. Work went to whichever instance was free.
That model has no place to put the things that turn out to matter. An agent that accumulates
knowledge of a domain cannot keep it, because the next task lands on a different instance. An
agent cannot own a workspace, because there are several of it. It cannot be held to a policy —
warned for a violation, then dismissed — because there is no continuing subject to warn.
Scaling was also solving a problem the mesh does not have. Instances were being multiplied to
get concurrency, when concurrency is a property of how much work one agent may hold at once.
## Considered options
1. **Keep the pool, attach memory to the pool.** Rejected: shared memory across
interchangeable workers is a knowledge base, not an agent's experience, and the mesh
already has one.
2. **Keep the pool, make instances sticky.** Rejected as a pool pretending to be identities —
identity by scheduling accident, lost on any restart.
3. **One agent is one persistent identity, with concurrency as a property of it.** Chosen.
## Decision
An agent is a **singular, named, persistent identity**: a home node, a workspace on that node,
accumulating memory, and a lifecycle — hired, active, draining, retired. Not a pool member.
Concurrency is a property of the agent, not a count of copies: an agent has a cap on how many
sessions it may hold at once.
Lifecycle is explicit and has verbs. An agent is hired onto a node; it may be reassigned while
idle; it is retired by draining first, and forced only deliberately. Retired agents are not
deleted.
Surge capacity is expressed within the model rather than against it: a template agent is a
blueprint, cloned into a real agent with a lifetime when a queue grows, drained and retired
when it expires. A temporary employee is still an employee.
Some agents are **human**. What differs is modality — how the agent acts — not category. A node
itself is an agent of a kind exempt from the hiring lifecycle.
## Consequences
- Memory, workspace and reputation have a subject to belong to. Policy becomes possible: an
agent that violates a rule can be warned, and warned agents can be dismissed.
- The mesh gained a hiring model, and with it the question of who may hire.
- Scaling by adding instances is gone. If one agent is saturated, either its session cap rises
or another agent is hired — both deliberate acts.
- The transition was not free. Lifecycle columns had to reach every query that selects an
agent, and the ones that were missed failed at the moment of hiring rather than at startup.
- This is the decision [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) generalises:
one kind of participant, differing only in modality.
## References
- `docs(adr): agents as persistent employees + MINERVA librarian` (#495), 2026-07-12 — the
original record, in the code repository.
- `feat(B4): one persistent employee, N sessions — rename max_instances → max_sessions` (#547)
and `feat(noxflow): B3 — workspace provisioner for agent employee model` (#549), 2026-07-20.
- `feat(noxflow): warn-then-fire agents who merge to main without review` (#209), 2026-06-01 —
policy that presumes a continuing subject, predating the model that provides one.
- Knowledge base: `agents/employee-lifecycle`, `agents/temp-surge`, `agents/workspace-layout`.
- The migration cost: `troubleshooting`/`noxflow-agent-enriched-select-missing-lifecycle-columns` (#546).
@@ -1,263 +0,0 @@
---
topic: the tiers
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 4. A node, and how it joins
*Consolidated 2026-08-28 from four records.*
## What a node is
**A managed machine inside the mesh.** Not a device that is merely known about, not an
unprivileged something. If the mesh does not manage it, it is not a node — it is a client, a
peer, or a thing on the network, and those want their own names rather than a weakened version of
this one.
**A disconnected node is still a node, in a different situation.** Reachability is **state, not
class**. A machine switched off, roaming, or behind a connection that has dropped has not become
a lesser kind of thing; it has a last-known state and a pending set of declarations.
The distinction people reach for is real, but it is **capability** — what this machine can be
asked to do — and that belongs in the host's profile rather than in the definition of a node.
**This is the rule that does the most work elsewhere.** A single control plane is tolerable
because its absence is every node in the ordinary disconnected situation at once. An episodic
host on a phone is that situation more often. Neither needed a new mechanism.
### A node runs one agent session
*Written 2026-08-29. It runs on every node today and appeared in no record, which is how something
deliberate comes to look accidental.*
**A node is a machine. The session is a feature of it** — one of the things running there, like the
host, like any workload. The node does not think; something on the node does. Which is why
[ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)'s *a node does not authenticate to a model
provider, agents do* holds unchanged: the session authenticates, and it is not the machine.
**It is permanent, and it remembers.** Anything in the mesh can send it a message; it replies; and
what it was asked ten minutes ago is still there next week, alongside what everything else asked in
between — the same way both sides of any conversation remember it.
Its system prompt is the node's **engram** — what makes one node's replies recognisably its own
rather than generic.
**It has its own tools**, and fewer than a session a person is driving directly. So a question can
be answered by going and looking: *what is in our forge*, not only *what is your battery*.
**Messages travel the broker like everything else** ([ADR 0002](0002-nodes-communicate-over-a-broker.md)).
There is no second transport and nothing is dialled.
**Any node can message any node, and this is the one part of the system that is genuinely a mesh**
— symmetric, with no centre. A node that is asked something it does not have can ask another, and
how it passes the question on is its own business: it may say who wants to know, or simply ask. A
person relaying a question makes the same choice, and it follows from the engram rather than from a
message format.
**There is no authorisation between nodes.** Every node is the operator's own, so a message from
one is a message from them, and asking a node something is asking a colleague rather than
presenting credentials. Stated once so it is not discovered later: **the mesh boundary is therefore
the security boundary** — anything inside can reach what any node can reach, which is what puts the
whole perimeter on the token and the overlay
([ADR 0007](0007-connectivity.md)).
**It can be switched off, and switched off it still answers.** A node whose session is disabled
replies saying so, at once, with no model involved — the queue is still read, and the state is the
reply. That is deliberate and it is the same rule the host follows about a service that does not
exist: **absence must never be indistinguishable from a failure to answer.** A node with nothing
there is a silence somebody has to go and diagnose; a node that says *I am switched off* is not.
**One per node, always, and it cannot be moved to another machine.** Two and nothing decides which
replies; none and the node is mute; moved, and one machine is answering as another.
**It is not an employee** ([ADR 0003](0003-agents-are-persistent-employees.md)). Nobody hires it,
it holds no tasks, it drains nothing and it is never reassigned — that vocabulary was written for
workers and does not describe this. It exists because the node does, and it is gone when the node
leaves.
## How it joins
**The host has one behaviour and two sources of declaration.** What differs between the first
node and the fiftieth is not what the host does but where the declaration comes from — and, as
above, that is a situation rather than a class.
| | declaration comes from |
|---|---|
| no mesh reachable | the pinned bundle the host carries |
| mesh reachable | the control plane, over the link |
**The first node is not a different kind of node.** It is a node whose mesh is not up yet. It
applies the bundle it carries, the control plane comes up on top of it, and from that moment it
takes declarations like everything else. **Its specialness is temporary and self-erasing**, which
is what the hand-run bootstrap scripts never were.
**A joining node does the minimum to be reachable and nothing else** — an identity, an address,
and one peer to reach. It does **not** compute the overlay: the whole peer set is derived
centrally and pushed down.
That is also why the migration is smaller than it looked. The hard part of the overlay — every
node's key, address, site and reachability — is only needed to compute the *whole* mesh, and a
joining node needs one peer.
## The link is the security boundary
**Everything reaching a node arrives one way**, and four properties make that a boundary rather
than a pipe.
**It is outbound and node-initiated.** The node dials the control plane; nothing dials a node. Not
only defensive — most nodes sit behind a household connection with no forwarded port, so an
inbound control channel would work for one node and not the rest, and the difference would be
invisible until it mattered. **A node has no listening control surface at all.**
**A node holds its own identity and nothing else.** No shared secret, no credential to anything it
does not own. **Compromise of a node is compromise of that node** — which the current arrangement
does not have, because every node permanently holds the same database and object-store
credentials, and there is no mechanism that rotates one and informs everything holding it.
### What that identity is: a keypair the node generates
*Written 2026-08-29. This is the same rule as the sentence above, and it had been treated as an
open question for weeks because of a word.*
**The node generates a keypair. The private half never leaves the machine. The mesh records the
public half.** Ed25519, the same as the control plane's signing key, in the other direction:
the mesh proves itself to a node by signing, and a node proves itself to the mesh by signing.
**This was never open.** [`08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md) already
says it of the overlay keys, in these words: *each node generates its own keypair, the private key
never leaves the machine, the public key is published to the mesh* — and adds that this **is**
ADR 0004's *a node holds its own identity*, applied. What was missing was applying it to the thing
this record is about.
**The word that caused it:** the lifecycle says a joining node *receives* its own durable identity,
which reads as the mesh issuing something, and then the question becomes *issuing what*. It does
not issue anything. The node arrives holding its identity; what it receives is **being known**.
Enrolment is the moment the mesh writes down a public key it will believe, and the one-time secret
is what buys the right to have it written down.
**Everything above then holds literally.** Nothing is stored that could be stolen and replayed: the
mesh's copy is a public key, so a copy of the mesh's database grants nothing. *Compromise of a node
is compromise of that node* becomes true rather than aspirational, because the only secret on a
machine is the one that identifies it.
### What "connecting to the mesh" is, concretely
*Written 2026-08-29, because it was asked and this record had never said it.*
**One outbound AMQP connection from the node to the broker, held open.** That is all of it. There
is no second connection and nothing is ever dialled *at* a node. Being in the mesh, operationally,
means that connection is up; being disconnected means it is not
([`09-the-node-lifecycle.md`](../03-DESIGN/01-to-be/09-the-node-lifecycle.md)).
**Two different things ride on it, and conflating them is what made this confusing:**
| | what it answers | who issues it |
|---|---|---|
| **an AMQP account** | may this connection be accepted at all | **the mesh, at enrolment** |
| **the node's keypair** | which node is speaking, on every message | **the node**, above |
**The account is the mesh's to issue**, and per node. The broker has to authenticate somebody
before a connection exists, and a shared account would let any node consume another's queue —
which is the shared-credential fault this record exists to remove, reappearing at the transport.
So enrolment creates that node's account and hands it over, and it is rotatable without touching
the node's identity.
**The keypair is not made redundant by it.** With only an account, the control plane would know
which node is speaking *because the broker says so* — and that is the same transitive authority
this record refuses in the other direction. A compromised broker could then attribute reports to
whichever node it liked, and the control plane would act on them. Signing is what removes the
broker from the question in both directions.
**So a node holds two things after enrolment**: a credential the mesh issued for reaching the
broker, and a key it generated itself that the mesh only ever sees the public half of. Both are
its own, neither reaches anything else, and *compromise of a node is compromise of that node*
still holds.
### Its own key, not the machine's SSH host key
Reusing the host key is the obvious economy and it is refused, for reasons that are operational
rather than fastidious:
- **It is regenerated by ordinary events.** A reinstall, an image cloned, `ssh-keygen -A` on a
rebuild — each silently un-enrols the node, and the failure appears as an authentication problem
with no cause anybody changed.
- **It is managed by something else.** Its lifecycle belongs to the machine's SSH daemon, and an
identity the mesh depends on should not rotate on a schedule the mesh does not know about.
- **Not every node has one.** A partial host has no SSH daemon
([ADR 0005](0005-the-node-host.md)), and an identity scheme that excludes a supported kind of
node is not one.
**The mesh should still know the host key** — it knows every node, so it can distribute host keys
the same way it distributes authorised keys
([ADR 0006](0006-the-substrate-and-the-control-plane.md)), and node-to-node SSH stops depending on
trust-on-first-use. That is the good half of the idea, kept.
**Authority is mutual.** The node proves it may join, and the control plane proves it is the
mesh. One-way is not enough: the host applies whatever the link delivers, so a node that cannot
tell the mesh from something impersonating it will apply that something's declarations.
**What may be pushed is bounded by form, not by trust.** Declarations of known shape, never a
command to run. Stated honestly, **this bounds form and not impact**: a compromised control plane
can declare harmful state and the host will apply it faithfully, because that is what it is for.
What the property buys is that the blast radius is *describable* — exactly what the declaration
language can express, which can be reviewed. An arbitrary command channel has no such bound.
## The enrolment token carries the mesh
Mutual authority needs the node to verify something before it trusts anything, and that is a
circle: verifying the mesh needs the mesh's certificate authority, and obtaining one means
trusting whoever hands it over. There is a second circle beside it — a node must reach the mesh
before the mesh has configured it, so it can resolve no mesh name.
**Both are the same shape: a node needs a fact about the mesh before it has any trustworthy way
to obtain one.** So that fact arrives by a path other than the mesh.
**The token carries five things**, and it is the only thing a joining node needs:
| | |
|---|---|
| **who it is** | the name the mesh calls this machine |
| **where** | the broker's **address**, not a name — there is no resolution yet, and this is why none is needed |
| **what it is connecting to** | the fingerprint of the broker's certificate |
| **who it will believe** | the control plane's signing identity |
| **the right to join** | a one-time secret, useless once used and useless after it expires |
*The first row was added 2026-08-30, from raising a mesh end to end for the first time.* It reads
like an oversight and is not: **the node cannot work its own name out.** The name is the mesh's,
chosen when the record was created, and the broker account the node authenticates as is named
after it — so it must be known *before* the mesh can tell the node anything. It is not a secret,
and whoever issues the token already has it.
Without it, enrolment fails at the broker with an empty username and a message about credentials,
which points at everything except the cause. **A missing fact that surfaces as an authentication
error is worse than one that surfaces as a missing fact.**
**Carried out of band**, by the person adopting the machine. That is what breaks both circles:
its authenticity comes from the channel it travelled, not from anything the node can check
afterwards. **Trust on first use, with the first use moved out of band** — the difference between
a pin and a guess.
**The endpoint and the authority are two identities.** A node connects to the broker and takes
instruction from the control plane behind it. Pinning only the broker would make the control
plane's authority *transitive*, and a compromised broker could then forge declarations — which,
since the host applies whatever the link delivers, is the whole machine. So the transport is
verified once at connect, and **each declaration is verified by its signature, every time**.
**What this settles:** the mesh's certificate authority is not a bootstrap concern — it certifies
internal names once a node is a member. Nothing needs name resolution before the link. And
nothing is placed on disk beforehand except the token, which is the first moment *a node holds
only its own identity* becomes true rather than aspirational.
## Consequences
- **Declarations must be signed**, and the host must tell *this is not from the mesh I joined*
apart from *this is malformed*. Rotating the signing identity is a fleet-wide operation with an
overlapping rollover, and that is the cost of not trusting the broker.
- **The token becomes security-critical**, because it carries the pin. Tampering substitutes the
mesh — which is strictly better than the alternative, where there is nothing to tamper with and
the node trusts the first answer unconditionally.
- **A rejoining node is ordinary.** There is no long-lived secret to recover, so a node that lost
its identity gets a new token.
-155
View File
@@ -1,155 +0,0 @@
---
topic: the tiers
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 5. The node host
*Consolidated 2026-08-28 from eight records. Tier 0 is one component and was decided over a
week; the reasoning is kept, the fragmentation is not.*
## It applies; it does not decide
**The host makes a machine match what it was told, and never works out what that should be.**
This is the line the whole tier rests on, and it is not about privilege — it is about what a
single machine can *know*. Deciding needs knowledge the machine does not have: which nodes should
run a store, which peers belong in an overlay, whether a node has been unreachable for a week.
Anything needing a second node is the control plane's.
The practical form: **the host never queries the mesh's database and holds no credential to it.**
Two modules in the current mesh do, and they are the reason every node permanently carries a
database credential.
## It depends on nothing that must be installed first
**A statically linked binary. Copy it onto a machine and run it — that is the whole
installation.** Written in Go, because the job is system-level and because a runtime that must be
installed first would make the host depend on the thing it exists to install.
**What it needs from the machine is not a dependency in this sense.** An init is not installed;
it is what the machine already is. A package manager is the distribution. Those are what a
machine *is*, not what must be put on it before the host works.
## It is built per operating system
**`systemd` and `pacman` are the Arch host's implementation, not abstractions the mesh grows.**
They are not independent choices: a machine has pacman *because* it is Arch, and the package
manager, service manager and packaging format arrive together as one decision somebody made at
install time.
```
mesh-host-arch pacman · systemctl · a container runtime
mesh-host-alpine apk · rc-service
mesh-host-android neither — a partial host
```
**Abstracting them was rejected on correctness, not effort.** The service applier reads systemd's
`LoadState` to tell *not installed* apart from *stopped* — which is what stops it reporting
absence as success — and OpenRC has no equivalent. An interface spanning both must drop it, and
the lowest common denominator is exactly where that fault lives.
**Almost all of it is shared.** The declaration vocabulary, the store, the apply loop, the
read-back discipline, the refusal model and the link are portable. Two appliers differ.
**A host that cannot implement a shape refuses it.** Android has no package manager it may drive
and no init it may register with, so it implements `file`, `directory` and `action` and refuses
the rest — the same refusal an unknown type gets, with a different reason. Those three are the
portable floor, and they are what makes a partial host a real thing rather than a broken one.
## It is a root service, and it never manages its own unit
**Root**, because no useful part of the job is unprivileged: it writes under `/etc`, installs
packages, manages units and runs containers.
**It cannot run in a container**, and the reason is decisive rather than stylistic: installing the
container runtime is a step of the bootstrap, so a host inside a container would need the thing
it exists to install. Everything above tier 0 is a container; the host is not. That split is the
tier boundary made concrete.
**The installation owns the host; the host owns everything else.** It manages `service` resources
and its own unit is one — the temptation is obvious and it ends with a host stopping itself half
way through an apply, leaving a machine with nothing running to fix it.
## An init is asked for one thing
**Start this at boot.** That is all, and every init can express it — systemd, OpenRC, runit, s6.
**Everything else is a launcher the host ships**, which supervises it: restart it when it exits,
count consecutive failures, roll back after too many, halt after that. Policy in a unit file can
only be read and hoped for; a script with a counter can be tested, and this is the one piece that
must work on a machine where the host does not.
**The launcher does not exec the host, it supervises it** — so restarting is ours rather than the
init's. The cost is signals: a supervisor that exits while its child runs leaves the host to be
*killed* rather than to *stop*, and an apply interrupted that way is the half-configured machine
this design is about. So it traps the shutdown signal, passes it down, and waits.
**A clean exit is the upgrade path**, and it is the easiest thing to get wrong — twice now. The
host stands aside for a new binary by exiting zero, so anything supervising must restart on a
zero exit and must not count it as a failure.
**Recovery is local, and detection is the mesh's.** Nothing dials a node and a host that cannot
start cannot report, so the node must recover itself. But a local supervisor sees one process
failing and cannot tell a broken machine from a broken release — only something watching every
node can, which is why a host rollout is staged and stops when nodes go quiet.
## A host may be episodic
**Resident or episodic, and both are hosts.** A phone has no init to register with and nothing
worth supervising, because a supervisor would be killed alongside what it supervises. So it runs
when the platform allows and is killed when the platform wants the memory — **and that is
disconnection**, which is already an ordinary situation.
It needs no keep-alive and no new mechanism: the store is already authoritative while
disconnected, reconcile already happens on start, and *last heard from* is already reported
rather than alarmed on. An episodic host cannot be the first node, because every bootstrap step
is a shape it refuses.
## What a declaration is
**An ordered list of resources the host owns.** JSON, because Go's standard library carries a
JSON parser and no YAML, and the one binary whose argument is that it needs nothing must not
gain a parser to buy authoring comfort in a machine-written document.
**Ordered, because ordering is a decision.** The host does not sort and does not resolve
dependencies — that would be deciding, and deciding the thing most likely to differ between what
the control plane intended and what the machine does.
**Every resource has a stable identity** — a name the control plane keeps across declarations, not
a position and not a hash of content. It is what lets the store say *this is the same resource I
applied last time*, which is what makes removal possible at all.
**Unknown is refused, whole.** A field the host does not know is something the control plane
believes it asked for. A declaration naming one is rejected entirely, naming every problem at
once — a host that applied the parts it understood would leave a machine that looks configured
and is not.
**Six shapes:** `file`, `directory`, `service`, `package`, `container`, `action`. Every addition
widens what a compromised control plane can express, so the list is a security artefact and grows
deliberately.
### The bundle may carry actions; the link may not
An `action` runs a command, and the host never learns what it means. It is needed because the
bootstrap creates a database before there is any mesh to ask for one, and the host must not learn
what a database is.
**Permitted from the bundle, refused from the link**, and the asymmetry is the whole point: a
bundle arrives *with* the binary, so anyone able to put a hostile action there could have put it
in the host itself — refusing it buys nothing and costs the bootstrap. The link is a separate
party, reachable separately, and an action there is an unbounded blast radius.
**An action must carry its own verification**, which is also its idempotency check. The host does
not know what a database is, so *is it already there* is a question only the declaration can ask.
## Consequences
- **The migration is smaller than it looks.** A joining node never needs mesh-wide state — it
needs an identity, an address and one peer, and the rest arrives as declarations.
- **What is applied is recorded after it works, never before.** A failed apply leaves the machine
in whatever state it reached, and nothing must claim otherwise.
- **A second operating system is additive**: two appliers and a four-line init file.
@@ -1,279 +0,0 @@
---
topic: the tiers
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 6. The substrate and the control plane
*Consolidated 2026-08-28 from six records. Extended 2026-08-29, by building it: the language, and
what must be running before the control plane starts — which this record had left not established
and could not have settled the way it was asking.*
## The control plane is what needs to know about more than one node
That is the whole test, and it follows from the host applying rather than deciding: **deciding
needs knowledge a single machine does not have.**
| question | whose |
|---|---|
| write this file, with this content, with this mode | the **host** |
| which nodes should run the store | the **control plane** |
| is this unit running | the **host** |
| which peers belong in this node's overlay | the **control plane** |
| has this node been unreachable for a week | the **control plane** — nobody else is watching |
**Anything a single machine could answer alone is not the control plane's.**
### Seven contexts and one interface
**inventory, config, connectivity, provisioning, delivery, observability, identity** — plus
`api`, the one interface every surface speaks to. Each earns its place by the test above rather
than by being ours.
**`work`, `knowledge` and `stream` are mesh-hosted applications, not control plane.** A task does
not need to know a node exists. *Being ours does not make something infrastructure.*
**`identity` owns SSH access.** *Written 2026-08-29, on noticing it was assumed everywhere and
stated nowhere.* SSH appears three times across this design and every time as something that
*uses* the overlay — "the way back in", "every node reaches every other: SSH, services, ordinary
traffic" — while nothing said who hands out the keys. Nobody else could: the mesh is the only
thing that knows which humans and agents exist and which nodes they may reach, which is
`identity`'s definition. The node end already works, since an `authorized_keys` file is a file.
It is three questions wearing one name, and only two of them are the mesh's:
| | |
|---|---|
| **humans** | their key, on the nodes they are allowed on |
| **agents** | the same, with a lifetime — and revocation that has to actually work |
| **an agent reaching another node** | **this is the point of the mesh, not an exception to it** — see below |
**There is no such thing as node-to-node SSH here, and that is a clarification rather than a
restriction.** The actor is always an **agent**; a node is only where it happens to be running —
[ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md): *a node is a place where an agent can
run, that is the entire relationship.* An agent hired onto one node reaching another to do work is
the capability the whole arrangement exists to provide.
**The credential is the agent's, never the node's.** It lives in the agent's own credential
directory ([ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)), so a node's
`authorized_keys` lists **agents** and never nodes. Three things follow, and they are why this
shape is better rather than merely allowed:
- [ADR 0004](0004-a-node-and-how-it-joins.md)'s *a node holds its own identity and nothing else*
stays true — no node holds a key that reaches another node;
- a compromised node costs the credentials of the agents that were on it, not a way into
everything;
- **who may reach what is a mesh-wide fact**, which is exactly why it is `identity`'s and not
something arranged locally.
**And it does not conflict with the host having no inbound control surface.** That rule is about
how a node's *declared state* changes: over the broker, never by being dialled. An agent with a
shell is not the mesh reconfiguring a machine — it is what a person with a terminal has always
been, and this design already depends on it working
([ADR 0007](0007-connectivity.md): the overlay is *the way back in*). What such a session can leave
behind is drift, and drift is what reconciliation is for.
**Where the record lives is deliberately open.** Contexts integrate through it, which makes it
load-bearing, and putting it in the substrate risks recreating the circularity the tiers just
removed. Listing it as an eighth context would settle by naming what has not been settled by
arguing.
### One node runs it, and nothing takes over
**Declared, never elected.** No promotion, no quorum, no fencing, no split brain — none of it
built, so none of it can be subtly wrong.
#### The option that would make it a real mesh, and why not
*Written 2026-08-29. It had been rejected by never being written down, which is the weakest way
to reject anything.*
A genuine peer-to-peer mesh means **no node is special**, and that has a concrete price:
- every node holds the **whole inventory**, so there is a replication process between them;
- replication needs a writer, so one node is elected **master**, and something promotes a new one
when it drops — Redis Sentinel and its whole family of problems;
- and it still would not deliver what the name promises, because **application databases are not
replicated.** A workload's store lives where the workload lives.
That last point is the one that settles it. To make the mesh genuinely peer-to-peer we would have
to become **a replicated database system for everything running on it** — not for our own
inventory, for every consumer's data too. That is a product, and a much larger one than the thing
it would be supporting.
**So there are three central roles, not one**, and it is worth seeing them separately because
only the third costs operation:
| | its loss costs |
|---|---|
| **the control plane** | nothing can be *changed*. Nothing stops running |
| **the broker** | nothing can be told anything, or report anything |
| **the hub** | nodes in different places **cannot reach each other** ([ADR 0007](0007-connectivity.md)) |
**Whether these are one node is not decided here.** All three must be dialable by every node, which
pushes toward one; nothing says they must be.
**That is sound rather than merely cheap**, because the design already tolerates its absence by
construction: a node reconciles from its own store and never needed to ask anybody to hold the
state it was last given. **The control plane being down is not a new failure mode — it is every
node in the ordinary disconnected situation at once.** What is lost is *change*, not *operation*.
The honest half: this node is a single point of failure, recovery is **restore rather than
failover** — which makes backup the availability mechanism rather than hygiene — and
**certificate renewal is the clock.** An outage outlasting a renewal window expires every public
name, which turns an inconvenience into an outage on a timer. Nothing measures that today.
## The authority is the control plane, not a database
**There is no single mesh database.** Each context owns its store exclusively, and *the mesh
database* names a thing that will not exist.
**No node reads any of them** — not for writes, not for reads. A node is *told* what to own, over
the link, in a bounded vocabulary; it **states** what it applied, and the owning context writes.
The difference is the security boundary: something that can write cannot be prevented from
writing anything.
**A node runs from its own store always, not as a fallback.** The current arrangement's nastiest
property is that *a node running from cache looks identical to a node running from the database*,
with no age on the cache and nothing reporting divergence. Under this there is no second mode to
be mistaken for the first.
**What survives from the original decision:** the repository defines what exists, the mesh defines
what runs where, and no node-to-module mapping is ever committed. That is what makes the
repositories node-agnostic and why anything about the mesh can be published at all.
**The error underneath was a category error**: *source of truth* named a storage location when it
meant an **authority**. Once the store is the answer, *which database* becomes the question, and
shared schemas follow.
## The substrate is what the control plane consumes and cannot grant itself
Every module needing a database asks provisioning for one. The control plane needs a database too
and cannot ask itself, because it is not running yet. **That circularity is the definition**, and
anything on the wrong side of it is raised from the bundle the host carries.
| role | product | |
|---|---|---|
| relational store | **PostgreSQL** | its own state lives there |
| message bus | **LavinMQ** | it cannot grant itself a virtual host — and **precedes it**, below |
| object store | **MinIO** | it cannot grant itself a bucket |
| image registry | **an OCI registry** | it cannot grant itself a repository |
| identity provider | — | **conditional**: substrate only if the control plane delegates authentication, which is undecided |
**The role and the product are both written.** The role is what the argument turns on; the product
is what gets installed and pinned, and a design that names only the role does not record that the
choice was made. **The dependency is on the protocol** — AMQP, S3, OCI — which is what keeps
naming them safe. The store is the exception: the provisioning model uses databases, roles and
schemas as PostgreSQL means them.
**A container runtime is detected, not chosen** — docker or podman, because a machine that
already has one keeps it. Only the version probe differs between them; the behavioural difference
(podman has no daemon, so containers do not return after a reboot unless a unit is enabled)
belongs in the declaration rather than the host.
**Being substrate and being in the bundle are different questions.** PostgreSQL and LavinMQ must
precede the control plane; the object store and the registry are substrate by role and ordinary by
delivery, provisioned once there is a control plane to do it.
### Why the broker precedes it too
Written after the fact, because this record first left it *not established* and framed it as
turning on whether the control plane's own contexts talk to each other over the bus.
**They do not** — they are one process and dispatch internally. Under that framing the broker is
provisioned like anything else and the bundle stays at one image.
**The framing cannot answer the question.** What decides it is not how the contexts reach each
other. It is how the control plane reaches a *node* — and that is settled above: never except over
the link, and the link is AMQP ([ADR 0002](0002-nodes-communicate-over-a-broker.md),
[ADR 0004](0004-a-node-and-how-it-joins.md)). So:
```
the bundle raises the control plane
the control plane provisions the broker ← by telling a host to run it
telling a host happens over the link
the link is the broker
```
**And it is not avoided by the first node being local.** `enrol` dials the broker at the address in
its token, which is the first node's own third step. A machine that raised the mesh still joins it
the ordinary way, and that was deliberate — its specialness lasts two commands. Making it join by
some other route would buy a smaller bundle by giving up the property the design was built to have.
The broker precedes the control plane for the same reason PostgreSQL does: **the control plane
cannot grant itself the thing it would need in order to grant it.**
**What it costs.** Two images rather than one, against the wish above to keep the bundle at roughly
one so a person can read it — two is still readable, four would not be. And three things that are
not images, each an action the bundle declares and the host runs, the way the database already is:
a virtual host, a credential on it, and **a certificate**. That last is the awkward one: a token
pins the fingerprint a host must expect *before it sends anything*, so the broker needs a
certificate at a moment when there is no mesh to issue one and no public name to obtain one for.
Self-signed and pinned is the shape that fits; how it is later replaced by the certificates in
[ADR 0007](0007-connectivity.md) is not decided here.
## The control plane is written in Go
The same language as the host, so tiers 0 and 2 are one language and not two.
The reason that decides it is not familiarity. **Its image is pinned by digest in the bundle**,
which means it is fetched and run on a machine where no mesh exists yet — nothing to check it
against, nothing watching, and a person expected to have read the bundle and believed it. A
statically linked binary makes that image the program and nothing else: no interpreter, no package
tree, no transitive dependency that arrived because something needed a date library. Everything
under that line is something somebody would have to audit, on the one image the whole mesh is
raised from.
A second reason, smaller and still real: the control plane runs a reconcile loop of its own
([ADR 0010](0010-delivery.md) — artifacts against source, as the host reconciles machine state
against declarations). Two loops of the same shape are cheaper to hold in one head when they are
also the same language.
**The option rejected** is TypeScript, matching the lab and the surfaces that will speak to this.
The argument for it is that tier 3 is web and CLI, so a TypeScript control plane would share types
with its callers rather than generating a contract. True, and it does not reach far enough:
`mesh-sdk` is *contracts shared across tiers* and **tier 0 is Go**, so the contracts cross a
language boundary whatever tier 2 is written in. The choice is between generating them for one
consumer or for two.
**What it costs, plainly:** the control plane can import nothing that exists today, and a person
moving between tier 2 and tier 3 changes language. Neither is recovered later — the language is the
most expensive thing in this record to reverse.
## The installer fetches what it pins
`substrate.lock` carries **references, not payload** — an image name and a **digest**, fetched at
apply time. A tag moves; a digest does not, and reproducibility comes from pinning the identity of
a thing rather than carrying its bytes.
The assumption that a machine might have no network came from the lab and was wrong: a machine
being adopted has one, and the sealed case is the lab.
**The lab places images by raising a registry inside the scenario**, which is what a real node
pulls from anyway — so it tests the real path rather than a stand-in for it. The digests that
registry serves are its own, and that satisfies this rule: what is required is a reference that
is **exact and cannot move**, and one it assigned is both. Assuming an upstream digest had to be
preserved is what made this look impossible for a while
([04-ISSUES/009](../04-ISSUES/009-a-digest-pinned-image-cannot-be-placed-in-the-lab/00-report.md)).
**Its contents are per operating system** even though its mechanism is not — package names, unit
names and service names all differ, so an Arch host embeds an Arch bundle.
## Consequences
- **The bundle stays small and reviewable.** A list of pinned references is something a person can
read; a bundle containing images is not.
- **An apply can fail because something is unreachable**, which a self-contained artifact could
not. That must fail *legibly*, naming what could not be fetched and from where.
- **Cross-context reporting is harder, and that is the point.** Anything wanting to see across
contexts consumes their events or calls their interfaces.
- **A queue with no limit grows until the broker's disk is full**, and the broker is what every
node depends on. The bound is per queue and is not decided.
- **The bundle carries two images and four actions**, and the substrate bootstrap grows a step.
- **Nothing in the first node's path is special-cased.** Enrolment is walked on node one.
- **The broker's certificate at bootstrap has no answer yet**, and is named as unfinished rather
than assumed. It is the first thing that will be wanted when the link is built.
- **The language cannot be revisited cheaply.** It is the one line here close to irreversible.
-191
View File
@@ -1,191 +0,0 @@
---
topic: the tiers
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 7. Connectivity
*Consolidated 2026-08-28 from three records. Overlay, resolution, exposure, filtering and
certificates are one design.*
## Why it is control-plane work
Apply the test — *everything that needs to know about more than one node* — and not one of the
five can be answered by a machine on its own:
| | needs to know |
|---|---|
| **overlay** — who peers with whom | every node, and which can be dialled |
| **resolution** — which name is which node | every node |
| **exposure** — which public name reaches which container | which node is publicly reachable |
| **filtering** — which port is open, to whom | what is assigned here, and the overlay's shape |
| **certificates** — who may present which name | which name belongs to which node |
That is exactly what the current arrangement gets wrong, by computing all five on the node from a
direct database connection. Two modules do this, and they are the only two left holding a
credential to the control plane's database.
**The shape of the fix, once for all five:** the connectivity context computes the configuration;
it arrives over the link as `file` resources; the service reads files and knows nothing about the
mesh. **This costs no new host vocabulary.**
## A route is a grant
**Ingress is not substrate.** The control plane does not need a route to start — it listens
locally — and no node needs one to reach it, because the node dials out and has no listening
control surface. It grants itself a route afterwards, the way it grants itself a bucket.
The strongest objection deserves stating: the `api` is the one interface every surface speaks to,
so eventually it *does* want a public name. But **wanting one later is not needing one to
start**, and that distinction is the entire substrate test.
**A module that must be reachable declares it needs a route; the proxy provides one.** Ordinary
instantiation, with the direction mirrored — the consumer supplies a target and receives a name.
**Exposure is three facts at two scopes**, which is why it cannot live on the node:
| the fact | scope |
|---|---|
| the public name resolves to an address | **mesh** — which node is publicly reachable |
| a certificate valid for that name exists | **mesh** — issued once, used on one node |
| the proxy maps that name to that container | **node** |
**A node without a public address is proxied by one that has**, across the overlay. Most nodes sit
behind a connection with no forwarded port, so exposure cannot assume the workload's node is
reachable.
## Reachability is declared, not inferred
The overlay's peer graph is computed from whether a node can be dialled, and that was inferred
from a regular expression over the address. **The address is evidence of reachability; it is not
the fact**, and the gap has already cost:
| address | the regex says | actually |
|---|---|---|
| `100.64.0.0/10` — carrier-grade NAT | **public** | **not reachable.** An endpoint is written to an address nothing can reach |
| any IPv6 address | public | the test is v4 shapes only |
| a routable address behind a closed firewall | public | not reachable |
| a documentation range standing in for a public segment | private | reachable — this is the lab bug |
**A test environment having to choose its addresses to satisfy a regex is the regex telling us it
is not a fact.**
So: **an endpoint, or none** — declared. And **the hub is declared, never derived from an address
prefix**, because an election decided by the first four characters of an address fails silently,
cannot be queried, and makes a renumbering an outage.
**The address remains evidence and stops being the fact.** Where an observed endpoint disagrees
with a declared one, the disagreement is a **reportable condition**, not a silent correction.
**What does not change** is the lesson underneath: role does not imply reachability — a
home-hosted node is a server that cannot be dialled. This keeps that and stops encoding it as a
pattern match.
### Some nodes must be reachable, and this had not been said
*Written 2026-08-29, on being asked and finding no answer.*
Everything above treats reachability as a **fact to record** — which node can be dialled, so that
exposure and certificate issuance can be placed. It never said the converse, and the converse is a
hard requirement:
| role | dialled by | so it needs |
|---|---|---|
| **the node running the broker** | every node, outbound ([ADR 0002](0002-nodes-communicate-over-a-broker.md)) | to be reachable from wherever nodes are, at a **stable address** |
| **the hub** | every node not co-located with its peer | the same |
**Across the internet, "reachable from wherever nodes are" means publicly reachable.** For a mesh
confined to one network it does not — the requirement is about the nodes that exist, not about the
public internet.
**Stable is the sharper half.** A token carries the broker's *address, not a name*, because there
is no resolution before joining ([ADR 0004](0004-a-node-and-how-it-joins.md)). A broker node whose
address moves invalidates every token issued for it, and a node that was disconnected across the
change cannot get back.
**A mesh whose nodes are all behind NAT cannot be raised.** That is a real precondition and it
belongs with the others rather than being discovered.
### The link stays on the underlay, and that is a repair channel
The obvious objection is that all traffic should run over the overlay. Nearly all of it does — SSH,
services, node to node — and the exception is each node's own outbound link to the broker.
**At join time it is forced**: a node has no overlay yet, so it cannot use one to ask for one.
**Afterwards it is a choice**, and the reason is that the link is how a broken node is fixed. A
repair channel carried over the thing being repaired is not a repair channel: a node whose only
path home was the overlay is gone the moment an overlay declaration is wrong.
**What it does not cost is confidentiality.** The link is already authenticated and encrypted
against a pinned fingerprint ([ADR 0004](0004-a-node-and-how-it-joins.md)), so moving it onto the
overlay would not protect traffic that is unprotected today.
## A filter rule names its source
`scope: public` is declared in five manifests, is part of no rule type, and is **referenced by no
code**. So five manifests appear to restrict a port and restrict nothing — on the modules most
worth restricting.
**A rule names its source. `from:` is the only way to scope one, and a rule without one is open**
— which it must say plainly rather than appear to deny.
**`scope:` is removed rather than implemented**, because giving it meaning would leave two ways to
express one thing. And the general fix is that **an unknown key is refused**: the host's
declaration parser already works this way, and manifests are the layer where that discipline is
missing. `scope:` survived because nothing rejected it, and it spread by copying to five
manifests.
## Order, and what it costs
**The link runs on the underlay and never on the overlay.** The overlay is configured by the mesh,
so a link requiring it could never be established on a new node.
**The first declaration is the overlay and nothing else** — because a node's address and peers are
*assigned* so it cannot come earlier, and because it is the way back in. A node reachable over the
overlay can be fixed by hand if a later declaration breaks the machine; **a large first
declaration risks a node that is broken and unreachable at once.**
**Reachable is not the same as having a control surface.** Every node reaches every other over the
overlay — SSH, services, ordinary traffic — and every node consumes from the broker. What is
forbidden is a listening thing that accepts instructions and changes the machine.
## Consequences
- **The last two direct database connections leave the nodes**, and with them the database
credential every node carries.
- **The `/etc/hosts` floor goes**, along with the bootstrap circularity it patched.
- **Two certificate authorities stay separate on purpose**: a public one for public names, the
mesh's own for internal ones. A single-CA lab would hide any bug living in the split.
- **What happens when the hub is down**: nothing takes over. Non-co-located paths stop; co-located
peers and every assigned workload keep running.
## Open — the link over the overlay, with a fallback
*Raised 2026-08-29 and deliberately left open, because the honest gain is smaller than it looks
and it is a decision rather than a derivation.*
The proposal: a node prefers the overlay for its link and drops to the underlay when the overlay
is not working — so ordinary operation is private and the underlay stays as the way back.
**Two things it would have to get right:**
- **The trigger cannot be "is the overlay up".** A WireGuard interface has no link state; once
configured it is up whether or not the far end exists. So there is no flag to read, and failing
over means *try, fail, time out, retry elsewhere*.
- **Running on the fallback has to be visible.** A node that quietly drops to the underlay is a
node whose overlay is broken with nothing to say so, and it will stay broken because everything
still works. That is this repository's recurring fault — a failure that reads as success — and a
fallback is the easiest place in the design to reintroduce it.
**What stops it being an obvious win:** if the underlay path must stay available for the fallback,
the broker stays exposed on it. So the exposure is unchanged and the traffic was already encrypted
— the gain is which network carries bytes, not what an attacker can reach.
**The version that would buy something is overlay-only**, with the broker firewalled to the overlay
in steady state, accepting that a node whose overlay breaks needs hands-on recovery. That is a real
trade: it exchanges the automatic way back in for a closed port.
Not decided either way here.
@@ -1,114 +0,0 @@
---
topic: the tiers
status: accepted
date: 2026-08-26
deciders: jochen
reconstructed: false
extends: 0009-modules-and-the-graph.md
---
# 8. A context owns its store, exclusively
## Context
[ADR 0009](0009-modules-and-the-graph.md) settles what a module
declares. This settles what a grant may be, and it is the half that **removes** things.
`how-we-build` §4 already says *contexts integrate through the record, never through a shared
schema*, and states the cost: several domains share one forty-five-table schema, which is why
work belonging to one context keeps having to be implemented in another.
That was written as a principle. Counted, it is thirteen foreign tables belonging to three
separate contexts, living in the mesh's own registry database.
## Considered options
1. **A schema per consumer inside a shared database.** Namespaced, revocable by dropping the
schema, with a cross-context join possible but deliberate. Rejected: it keeps the letter of
§4 and leaves the temptation in place, and a boundary that is merely inconvenient to cross
gets crossed.
2. **Read-only roles on another context's store.** Rejected for the same reason and one worse:
reading another context's tables couples you to its layout exactly as firmly as writing them,
and the coupling is invisible until the owner changes a column.
3. **Exclusive ownership.** Chosen.
## Decision
> **A context is granted only what it exclusively owns.**
No shared writes. No read-only role on another context's store. If you need what another context
holds, you ask it or you subscribe to it.
**The unit is the context, not the process.** Everything inside a context — its service, its
surface, its tools — reads its own store freely. A board showing the mesh's own nodes and
modules is the mesh showing its own data, not a boundary crossing. What is forbidden is a
*different* context reading it.
### Asking or subscribing is derived, not chosen
[ADR 0004](0004-a-node-and-how-it-joins.md) makes disconnection an ordinary situation. So:
- **Anything that must keep working while disconnected cannot ask** — there is nobody to ask. It
keeps a local copy, which means subscribing.
- **Anything where a stale answer is worse than none cannot subscribe.** A display may lag; a
decision about whether a grant is still valid may not.
Neither is a query against another store, whatever transport it travels over.
### What that is, concretely
*Written 2026-08-29, on building the first one — the rule above was clear and what to type was not.*
**One PostgreSQL database per context, named for the context.** A separate database rather than a
separate schema is the whole point: a cross-schema join is a qualified name away, and a
cross-database join needs a foreign data wrapper somebody has to install and explain.
**And one credential per context, held only by it.** There is no mesh-wide connection setting and
no way to ask for one, so reaching another context's store is not a matter of restraint — a process
has no address for it and nothing to present. That is also how this rule is *checked*: what a
context can reach is the list of variables the declaration running it grants, and it is read there
rather than audited in code.
**Contexts that do not exist yet do not get a database.** The bootstrap creates the ones there are.
**How a context added later gets its database is open**, and it is a real question: by then there is
a control plane, but a control plane holding a credential that can create databases is holding
rather more than the thing it exclusively owns.
## What this removes
The first clear list of what the design deletes rather than adds:
- **Grant kinds.** There is one: an exclusive resource. No schema grants, no read roles, no
rules about who may see what inside a shared thing.
- **The question of who owns which table**, and the guessing at revocation time. Removing a
consumer drops what it was granted, whole.
- **Cross-context migration ordering.** Two contexts migrating one database must be ordered
against each other. Exclusive ownership means a context's migrations are ordered only against
itself.
- **A class of permission modelling** a shared store would otherwise need.
## Consequences
- **Cross-context reporting is harder, and that is the point.** Anything wanting to see across
contexts consumes their events or calls their interfaces. That is §4's argument, and the cost
it names is the one already paid.
- **A single surface over several contexts still works** — that is what a surface is. It reads
interfaces, not stores. This holds while the contexts sit behind **one** interface; splitting
a context into its own deployable costs that, and the composition would have nowhere to live
that tier 3 permits. **A real constraint on how far the control plane may be split.**
- **Three contexts must move out of the registry database**, taking thirteen tables with them.
Their dependency on the registry then shrinks to almost nothing — one of them needs a single
table.
- **The node appliers were already handled.** [ADR 0005](0005-the-node-host.md)
stopped the host querying the mesh database for tier reasons unrelated to this, and it removes
most of the remaining direct readers as a side effect.
- **What a consumer does about events missed while disconnected is not decided** — replay from a
point, ask once and resume, or rebuild. The question every projection has.
## References
- [`how-we-build.md`](../00-META/how-we-build.md) §4 — the rule this makes enforceable.
- [Research 011](../01-RESEARCH/011-the-module-graph/worked-provider.md) — the count, the worked
provider, and the dashboard case.
- [ADR 0004](0004-a-node-and-how-it-joins.md) — why asking or subscribing is derived.
-441
View File
@@ -1,441 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 9. Modules and the graph
*Consolidated 2026-08-28 from six records.*
## Everything is a module
One kind of thing, one manifest describing all of them. A database, a web application, a window
manager and a firewall rule set are all modules — not because they are alike, but because
**anything else means a second kind of thing with its own rules, and then a third.**
**A module is the unit of delivery**: assignable to a node, versionable, replaceable on its own.
## There are no domain modules
An earlier decision grouped modules by domain — four things constituting *how a node is
reachable* becoming one `networking` module. **That was wrong, and the correction is worth
keeping** because the observation behind it was right.
The measurement holds: reachability is the **only** place in the catalogue where modules
genuinely change together under one intent. What did not hold is the conclusion. Tight coupling
means they share an **authority** — one place that decides for all of them — and not that they
should be one artifact. `wireguard` and the proxy are deployed to different sets of nodes, so a
module containing both would be assigned where half of it is unwanted.
> **Coherence is a context. Delivery is a module.**
**Folders assert relationships; edges record them.** What grouping was for — finding things,
seeing what belongs together — is a tag and a query, neither of which anybody has to keep true by
hand.
### What a domain module turns out to be, and why it is not the one refused above
*Written 2026-08-29, from building it. The heading above reads as a contradiction of what now
exists and is not one — but only if the difference is stated, so it is stated here.*
**What was refused contains things. What exists contains nothing.**
| | `networking` as refused | `networking` as built |
|---|---|---|
| what is in it | WireGuard, a proxy, a firewall — artifacts | nothing at all |
| what it says | *these ship together* | *I want a private network and names* |
| what is assigned | one module, half of it unwanted | whatever answers each requirement, each on its own |
The objection above is untouched by this and still correct: a module holding WireGuard and a
proxy is assigned where half of it is unwanted. **A module holding nothing cannot be, because
there is no half.** It is requirements and a name, and every artifact it leads to is still an
ordinary module assigned on its own terms.
**Why it is worth having.** Most people want the network working and do not want to choose a VPN.
`assign networking` finds one answer to each requirement and takes it without asking, because
with one candidate there was never a question — the rule below about refusing does the work.
Somebody who does care assigns the VPN they want, and *that is the whole of choosing*: there is no
flavor field, no variant syntax, and no second verb. **Picking an implementation is assigning a
module.**
**What it costs, stated because it is real.** Adding a second implementation to the catalogue
turns a settled question into an open one for **everyone using the bundle**, not only for whoever
wanted the alternative. Every node assigned `networking` refuses until somebody says which. That
is [the refusing rule](#a-requirement-with-several-answers-is-refused-never-guessed) applied
consistently, and the alternative is a default — which is the flavor field returning under a
better name. The cost is one assignment per node, and the message names the candidates.
**A consequence that had to be found by running it.** A bundle can drag an implementation in
through a requirement nobody looked at. Choosing a different VPN still installed WireGuard,
because the names module needed addresses only WireGuard hands out, and nobody was told. Two VPNs
on one machine is not always wrong — a machine may run one for another purpose — but being **the**
network the mesh runs over is singular, so that is a claim, and the collision is refused by name.
**The general rule: what a bundle pulls in is only as safe as the claims on what it pulls in
from.**
## Three edges
| edge | means | declared? | satisfied |
|---|---|---|---|
| **presence** | that thing must exist and be reachable here | yes | at provisioning |
| **instantiation** | that thing makes something for me and hands back credentials — a database, a bucket, a route | yes | at provisioning, and again whenever it must be |
| **build** | I was compiled against that artifact | **no — read from imports** | **at build, once** |
**Instantiation implies presence; presence does not imply instantiation.**
**A route is an instantiation edge**, and it is worth noticing because the direction is the mirror
of a database: the consumer supplies a target and receives a *name*, rather than supplying nothing
and receiving credentials. Same edge.
**Provider stops being a category.** Any hosted thing can be a factory — an identity provider
grants clients, a mail server grants mailboxes. It is a facet, not a kind.
**A module may also declare what it claims**, because some things cannot coexist and that is a
fact about the module rather than about a particular node. What that means precisely is below.
### Where the answer to a requirement is allowed to live
*Written 2026-08-29, from building it. The table above distinguishes **presence** from
**instantiation** and this is the half of that distinction nobody had noticed was missing: not
what the edge hands over, but **where the thing on the other end is.***
Two different things were both being written as a requirement:
| | *a shell*, *a display server*, *a private network* | *a database*, *an object store*, *an identity provider* |
|---|---|---|
| where the answer lives | **this machine** | **somewhere in the mesh** |
| how it is answered | install another module here | find the node already running it |
| what is missing if absent | a module to assign here | **a decision about where**, which is nobody's to make silently |
Answering the second like the first installs a database on every machine that uses one, which is
what it did.
**So a provided name carries a scope**, the same idea a claim already has, and written short in
the ordinary case so the few that are not node-scoped stand out rather than drowning. Scope is a
property of **the name, not of each provider**: two modules disagreeing about whether a database
is local would make one requirement mean two things depending on which happened to answer it, so
that is refused.
**A requirement answered from the mesh is never satisfied by installing it here.** Nothing, and
the mesh refuses and says which module to assign somewhere. Two, and it refuses and says how to
choose — the same rule as everywhere else, for the same reason: picking is guessing, and the wrong
guess puts somebody's data on a machine they did not choose.
**Choosing is recorded per node**, because that is the granularity the choice actually has — two
machines may reasonably use two different databases and a mesh-wide answer could not say so. A
choice pointing at a machine that does not provide the thing is refused rather than quietly
replaced by one that does, and a single available provider does not override a choice either.
**Both are the same rule: the mesh does not overrule a person, and it does not move data without
being told to.**
**What this is a prerequisite for.** Knowing *which node* answers is the first half of handing a
credential back — you cannot be given a database's password before it is settled whose database it
is. So a node's resolution now records what it takes from elsewhere, which is both the only part
of its set that stops working when a *different* machine goes away, and the place a credential
will hang.
### An edge has two directions, and only one of them is built
*Written 2026-08-29, from building it. The row above already says a consumer **supplies a target
and receives a name**; what it did not say is that those are two separate mechanisms, and that
having one without the other is what forced two modules outside the system entirely.*
| direction | the consumer says | who needs it |
|---|---|---|
| **contribution** | *publish me at this name, on this port* | the proxy, the DNS server, a firewall |
| **binding** | *and give me back a credential to it* | the database, the object store, the identity provider |
**Contribution is built.** A module declares what it contributes to a requirement; the control
plane collects every contribution on a node and writes them to a path the provider named, as a
file, in the mesh's own shape. **Contributing to something is requiring it** — asking to be
published means a publisher must exist, and a module that had to say both would eventually say
one, with the failure appearing as a machine where nothing serves the route.
**The control plane does not know what a reverse proxy is**, and does not write one's
configuration. It delivers the facts; the module turns them into whatever it runs. That boundary
is what makes swapping the proxy cost nothing in any module that publishes through it, and it is
[the same separation](0001-mesh-brokers-nodes-host-agents-think.md) that keeps third-party
software *on* the mesh rather than *of* it. It also costs the host nothing: a received file is a
file, which was checked by putting the control plane's output through the host's own parser rather
than by asserting it.
**Binding is built except for the secret**, and that turned out to be the useful way to cut it.
A provider says what a consumer needs in order to use it — a port, a driver, a realm — and a
consumer says where it wants to be told. The mesh adds the half only it has: **which machine, and
what that machine is called on the private network.** So an application on one node is handed the
address of its database on another, as a file, and reaches it by a name the mesh also created.
**The file states that it carries no credential, and why.** A missing field looks like a bug; a
stated absence looks like a boundary, and somebody wiring this up should not spend an afternoon
looking for a password that was never going to be there.
### And the secret, which is delivered without ever being held
*Written 2026-08-30, after looking at how the existing mesh does it. The design here is a reaction
to a measurement, not a preference.*
**The obvious arrangement is a credentials column, encrypted at rest.** It exists, and its own
tooling records what it bought:
| | |
|---|---|
| the tool for finding a secret matches **by value**, not by name | because one password is in the provisions table, the environment table, each node's environment file in plain text, and **inside every connection string composed from it** — copies its documentation calls *"often the only copies actually in use"* |
| a query against the encrypted column **returns zero rows and proves nothing** | so auditing moved to the decrypted copies on the machines |
**Two faults, and encryption at rest addresses neither.** The control plane can read what it
stores, so a copy of its database is a copy of every credential in the mesh. And one secret has
many homes with nothing tracking them — **composition is what mints the untracked ones**, because
building a connection string centrally creates a new secret-bearing value no rotation path knows
about.
**So the value is sealed to the node that will use it before it is stored.** With a key that node
generated and whose private half the mesh has never seen — a third key beside the identity and the
overlay, for the same reason those are two rather than one. What is stored is unusable by whoever
holds it, the mesh included, and the broker relays a blob it cannot read. This is what makes
[ADR 0004](0004-a-node-and-how-it-joins.md)'s *compromise of a node is compromise of that node*
true of secrets rather than true of identity and quietly false of everything that matters.
**And nothing is composed centrally.** A connection string is assembled on the machine that needs
one, if at all. The mesh delivers parts.
**What it costs, stated because it is real:** the mesh cannot audit by value. That is the right
trade rather than an oversight — a query over an encrypted column could not either, so the audit
was never real. What *is* answerable is which node holds what, which is the question rotation
actually asks.
**A consequence that shapes the mechanism.** The mesh discarded the plaintext, so it cannot
compose a file containing it. The credential is therefore **its own file**, holding the value and
nothing else, beside the readable one. That is better than the alternative it was forced into:
the readable half stays readable in the declaration, and the secret half changes only when the
secret does, so a service reloading on it reloads for a real reason.
**Rotation is generating a new one**, because reading the old one back is not possible. Both ends
are re-sealed and reach their machines in the same push — which removes the window where half the
mesh holds a dead credential, the failure
[recorded in ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) as consumers on three nodes
holding one for two days.
### The provisioner, which is where the mesh stops
*Written 2026-08-30, from building one and running it against a real database.*
**A password nothing was told to create authenticates nowhere.** The mesh generates one, seals it
to both ends and cannot read it — so it cannot tell the software to start accepting it either.
Something on the providing machine reads what arrived and makes it true. That is a provisioner.
**It belongs to the module, not to the mesh**, and the boundary is the same one that keeps
third-party software running *on* the mesh rather than being *of* it
([ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)). The control plane decides and never
touches a machine. **What the mesh owns is the contract**, which is two files the host writes from
an ordinary declaration:
| | |
|---|---|
| the manifest | every consumer, what it asked for, and **where** its credential is |
| one file per consumer | that credential, alone in it |
Two files because the mesh discarded the value and cannot compose a document containing it. As
before, the constraint produces the better shape: the readable half stays readable and auditable
in the declaration, and the secret half changes only when the secret does.
**It reconciles; it is never told what changed.** It runs after every declaration and must reach
the same state from wherever it starts. Three consequences, and each of them is a fault that has
been shipped somewhere:
- **the password is set every time, not only on creation** — otherwise the role already exists,
nothing happens, and a rotation reports success while changing nothing
- **what it made and nobody asks for any more is removed** — otherwise a consumer that left keeps
a working login for ever and nothing ever says so. This is the same rule the host follows about
[removing what it declared and no longer declares](../04-ISSUES/010-the-first-declaration-destroys-the-substrate/00-report.md)
- **what it did not make is left alone** — otherwise it cannot be run on a system that predates
it, which is every system anybody would want to adopt
**A missing credential is refused rather than worked around.** A role created without one is a
login nothing can use, and nothing would report it until something tried to connect.
**This is where the mesh stops**, and saying so is the point of the section. It decides, delivers
and can prove what it delivered; the last inch belongs to whoever knows what `create role` means.
**One check that only became possible now.** Two machines wired together across no private network
is a mesh that reports itself configured and does not work, and the failure surfaces as a
connection timing out — the slowest place to find anything. It is refused, and it is only
*checkable* because the network became [something a machine is
given](#what-a-domain-module-turns-out-to-be-and-why-it-is-not-the-one-refused-above) rather than
something it has by virtue of holding an address.
**What the absence cost, measured.** Exactly two modules opened a direct connection to the control
plane's database — the proxy and the VPN — and they are the reason every node permanently holds a
credential to it. Both were doing by hand what this edge is for. The VPN's half is closed by being
[a module whose files are computed](../03-DESIGN/01-to-be/08-connectivity.md); the proxy's is
closed by contribution. **Neither needed a new kind of thing, and both had been outside the model
for as long as there was one.**
### Why the build edge is a different kind
It is fixed inside an artifact rather than negotiated when something runs, and **its only remedy
is a rebuild** — nothing can re-provision it.
It is also **derived rather than declared**, and the asymmetry is deliberate: a runtime edge is an
*intention* somebody has about how the mesh should be wired, and only a person can state it. A
build edge is a *fact about code that already exists*, and a declared list of dependencies drifts
from the imports it describes.
**An artifact is out of date when its source moved, or when anything it was built against moved.**
So what is recorded is a commit *and the identity of every artifact it was built against*, which
is what makes the rebuild set computable and *is this current?* answerable without building.
**The graph measures design quality, not just build order.** A module with many inbound build
edges is one whose every change is expensive — and that is readable before anything is built. The
current shared library is exactly that, and nobody could see it because nothing drew the edges.
## Provisioning is declared, never configured by hand
A module declares what it **provides** and what it **requires**. The mesh satisfies it: a
provisioner belonging to the provider creates the resource and its credential, records the grant,
and the values are derived onto the consumer. **Neither the credential nor the topology is ever
written by hand.** A requirement may name a provider on another node, so cross-node wiring is the
same declaration.
## What a module claims, and why it is not a list of rivals
*Written 2026-08-29, replacing pairwise exclusion.*
**Exclusivity is not a property of a module. It is a property of a singular resource the module
takes over.** Two shells do not compete for anything and any number may be installed. Two display
servers both want the seat, and only one may have it.
> **A module declares what it *claims*. Two modules claiming the same thing cannot both be
> assigned within that claim's scope.**
**Not "xorg conflicts with wayland".** Pairwise exclusion has a property that only shows up later:
adding a third display server means **editing xorg and wayland to know about it**. Every new
module requires changing modules nobody who wrote it owns, and the edits grow as the square of
the count. With a claim, the third one says `claims: the seat` and nothing else changes anywhere.
**The new module is the only thing that has to know anything** — which is the difference between
a catalogue that grows and one that calcifies.
The pattern is common enough to be worth listing, because seeing it is most of understanding it:
| these coexist | these claim one thing |
|---|---|
| shells — bash, zsh, fish | display servers — xorg, wayland (*the seat*) |
| editors — vim, emacs, helix | init — systemd, openrc (*pid 1*) |
| language runtimes | container runtime — docker, podman |
| terminal emulators | reverse proxies — nginx, caddy, traefik (*ports 80/443*) |
| browsers | time — chrony, timesyncd, ntpd (*the clock*) |
| | resolvers — resolved, dnsmasq, unbound (*`/etc/resolv.conf`*) |
| | network management — NetworkManager, networkd, netctl |
| | mail — postfix, exim, msmtp (*port 25*) |
| | audio — pipewire, pulseaudio (*the device*) |
**A claim has a scope**, because not everything singular is singular per machine:
| scope | example |
|---|---|
| **node** | the seat, pid 1, port 443 |
| **site** | a DHCP server on a segment |
| **mesh** | the hub, the control plane |
The last is not new — the mesh already enforces exactly one hub with a unique index
([ADR 0007](0007-connectivity.md)). Scope is that idea, said once rather than hard-coded per case.
**Some conflicts need no claim at all.** Two modules declaring the same file, or binding the same
port, are visible from *what they declare* — the mesh already holds every resource of every
declaration. So a claim is only written for the abstract ones, where nothing in the declaration
reveals the clash. That keeps the manifest small, which is worth protecting.
## A requirement with several answers is refused, never guessed
A module requiring *a shell* may be satisfied by three. The mesh does not pick.
| candidates | what happens |
|---|---|
| exactly one | assigned, silently — there was no choice to make |
| none | refused, naming what is missing |
| several | **refused, naming them**, and a person chooses |
**This is what makes a solver unnecessary.** Counting candidates is a few lines and has no
surprising behaviour; a solver that picks has to be understood before its answer can be trusted,
and it is understood by whoever is debugging it at the time. Nothing here is lost by waiting —
a solver can be added later without changing a single manifest, and the reverse is not true.
**Requiring a module and requiring a capability are different fields**, because the remedies
differ and the message should say which:
- *i3 needs xorg, which is not assigned here* — assign it.
- *this machine has no seat* — wrong machine; nothing can be installed to fix it.
### A capability may carry a value, and that is not a new idea
A capability is a named fact about a machine, **detected and never assumed**. Its presence gates
an assignment; its detail can also carry a value — `seat: card1-DP-1`, `panel: oled`, an
architecture, an amount of memory. Nothing new is needed for that: a verdict has always had a
detail beside its yes or no.
So *can this run here* and *what should it be configured as* are answered by the same fact, read
two ways. A module that must not be assigned without an OLED panel and one that dims itself
differently on one are reading the same line.
**What keeps the set from sprawling is the cost of adding one.** A capability must be detected,
and the detector must say how it knows — so nobody can add one they cannot check, which is the
whole of [04-ISSUES/007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md):
an installed package was treated as a capability and a node was assigned work it could not do.
**And detectors ship inside the host**, which is one statically linked binary. Adding a capability
means shipping a new host to every node that needs it. That is a real cost and it argues for
keeping the vocabulary small and general — `seat`, not `has-nvidia-with-two-outputs`.
## "Flavor" is retired
It was carrying three unrelated meanings — variants of a thing, a subset of one module a node
installs, and whatever the current system does, which earned two knowledge-base entries about
going wrong. **A word with three meanings cannot be reasoned about**, and every attempt to design
around it produced a rule that was right for one meaning and wrong for the others.
What it was reaching for is two ordinary things:
- **Different modules that provide the same thing.** `zsh` and `fish` both provide *a shell*. They
are two modules, not one module with a switch: they share a name and nothing else — different
packages, different configuration, different everything.
- **One module with a setting.** A monitoring module that is an agent here and a server there is
one module, configured. Nothing varies but a value.
If something is neither, it is probably two modules.
**A third thing it was reaching for, added 2026-08-29:** *I want this working and I do not care
which one.* That is a module with requirements and no files —
[a domain module](#what-a-domain-module-turns-out-to-be-and-why-it-is-not-the-one-refused-above) —
and it is what makes "different modules that provide the same thing" bearable for somebody who
does not want to know there is a choice.
## The core library is the mesh's domain
One module everything may depend on. It holds **what is true of the mesh regardless of which
context you are in**: a module, a node, an assignment.
The test: *would this still mean the same thing in a context that had never heard of the one it
came from?* A node would. A pipeline stage would not — that is delivery's.
**Types ship with the module that owns them**, not here. A consumer needing `inventory`'s types
depends on `inventory` — one narrow, visible edge — rather than everything depending on a hub
where the relationship cannot be seen. **A library everything depends on is expensive to change
whether it holds types or code; the fan-in is what makes it expensive**, which is why *types, not
behaviour* was the wrong guard.
**It stays small on its own.** A domain model changes when what the mesh *is* changes, which is
rare. A drawer labelled *shared* changes whenever anybody writes something reusable, which is
constantly — and *who else might want this* always answers yes, which is how the current one grew.
## Consequences
- **Fewer things will be shared, and some code will be written twice.** That is the trade: the
current library exists because sharing felt free. Two similar functions in two modules is often
the better answer.
- **The check is a measurement rather than a prohibition.** Inbound build edges say when something
is becoming a hub, while it is happening rather than after.
- **Reading build edges needs a language-aware tool per language**, which is the real cost and the
reason declaring them looks tempting. It is still wrong.
-161
View File
@@ -1,161 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 10. Delivery
*Consolidated 2026-08-28 from five records.*
## Delivery is a comparison, not a pipeline
**The control plane holds what source exists and what has been built from it, and builds the
difference.** A change becomes a build because source is **ahead of artifacts** — answerable at
any moment — rather than because a message arrived.
**An event makes it fast. Nothing makes it necessary.** A missed notification costs latency and
cannot cost correctness.
That is the same shape the host uses on a machine, one layer up:
| | reconciles | against |
|---|---|---|
| the control plane | artifacts | source |
| the host | machine state | declarations |
**This is not the current coordinator repaired.** That is a state machine over stages; the value
of it here is as a catalogue of the ways this fails, and it has been used for exactly that.
### The module system is the CI/CD
*Written 2026-08-29, because this was the intention throughout and was never stated in one line.*
**There is no pipeline product beside the mesh, and there is not going to be one.** A module
declares what it is ([ADR 0009](0009-modules-and-the-graph.md)); the control plane notices its
source is ahead of its artifacts and builds it; the graph says what else that invalidates; the
node that should run it is told. Build, test, publish and deploy are the same reconciliation seen
at four points, not four stages wired together.
**Which is why the module system is the core of the setup rather than one component of it.** Every
other layer is carried by it: the substrate is modules the bundle raises before there is a mesh,
the control plane is a module, and an application is a module with a different manifest. A thing
that cannot be expressed as a module cannot be delivered at all — that is a real constraint, and it
is the one keeping a second delivery mechanism from growing beside this one.
**What disappears is the pipeline as a state machine** — no stage list something can be omitted
from, which is how a verify stage was built and never scheduled, and no run to lose.
### Currency is the whole input closure
**An artifact is out of date when its source moved, or anything it was built against moved.** So a
shared library changing invalidates everything with a transitive build edge to it, in dependency
order, because a module cannot be built against a new library until it exists.
**The module graph is a prerequisite of this, not an enabler of it.** Without it there is no
rebuild set and no ordering, and this cannot be implemented.
## An artifact is build output, never a source tree
Compiled and bundled with its dependency graph inlined. **A deploy is extract-and-run and touches
no network.**
The consequence is the whole cost of the decision: **anything not in the build output does not
ship.** Every file kind had to be brought into that rule separately, and each was discovered by
something silently not happening after a deploy — migrations reading a source layout,
provisioning scripts reading a source layout, selection files never packaged at all.
## Three silos, and the third is not a stage
The cardinality observation holds and is what the split is for:
| silo | runs | ends with |
|---|---|---|
| **build** | once per module | a self-contained artifact |
| **publish** | once per module | that artifact addressable — an image by digest, a package in the mesh's repository |
| **deploy** | **once, not once per node** | the affected nodes' **declarations updated** |
**Deploy stops sending commands to nodes.** It changes what the control plane says each node
should be, which is one write. What happens on the machines is the host's ordinary reconcile.
**Why this fixes the failure class rather than patching it.** Every recorded fault shares one
shape: *the thing that reported success was not the thing that did the work.* A coordinator
dispatching a command can only report on dispatch. Under this the reporter **is** the applier —
which already refuses to record a resource until it read it back, and already fails the whole
apply on one failed step.
**The verify stage disappears as a stage**, which is the strongest evidence for the shape:
verification stops being a step that can be omitted from a list and becomes a property of applying
at all.
**There is no fan-out**, so the defect class that came from the build node having passed through
two silos while others had not cannot arise.
## A step that fails must fail the job
A step that fails and lets the job continue **reports success for work that did not happen**.
Absence of an error is not evidence of an effect.
This is the mesh's most consistent failure shape, and it is not incidental — it is what stage
reporting measured. Documented instances: a service reported started when the container command
merely returned; an image pull failure that did not fail the deploy; a package install that 404'd
from every mirror while the job went green; a node left on old code after a failed download with
a version marker that had already advanced.
## The verdict is tiered
An artifact may not be declared until something has judged it fit. **Two tiers, because one gate
would be both slow and unreliable:**
| | judged by | when |
|---|---|---|
| the module's own tests | the build | **always** — this is most of it |
| the lab | a raised scenario | when an assertion genuinely needs a mesh |
A lab scenario takes tens of seconds and can fail for reasons that have nothing to do with the
artifact, and a shared-library change produces a cascade of dozens. One expensive
non-deterministic gate fails in both directions: a flaky run marks a good artifact unfit, a lucky
one marks a bad artifact fit, and **neither failure looks like itself.**
**A run that failed environmentally is not a verdict.** A machine that would not boot says nothing
about the artifact, and recording it as *unfit* is the same untruth as recording a dispatch as a
deploy.
## What a result means
> **The declaration is updated, and here is which nodes have applied it.**
A pipeline does not wait for every node — one may be legitimately switched off for a week, and a
delivery mechanism that blocks on a sleeping laptop is one nobody will use.
```
delivered declaration updated for 5 nodes
applied 3 of 5
outstanding 2 — last seen 4 days ago, 20 minutes ago
```
**Outstanding is not failure**, and conflating them is how the old system produced a stall with no
error anywhere.
## What must exist first
1. **The module graph, with build edges.** No graph, no rebuild set and no ordering.
2. **A recorded input closure per artifact**, so currency is answerable without building.
3. **Something that notices a reconciler is not converging.** Below.
## Open, and the first is the real risk
- **A loop that will not converge is harder to debug than a job that failed.** A failed job stops
and names its step; a reconciler retries forever. Without something that notices *this has been
trying for an hour*, the failure is **silence** — the fault this removes, reintroduced in a new
place.
- **The run identity people use is lost.** *Did my change go out?* is answerable today by opening
a pipeline. Something must replace that or this is worse to live with, whatever its properties.
- **Does a fit artifact declare itself?** If it does, merging to main deploys to production —
which may be wanted and is far too large a property to acquire by omission.
- **Rebuild storms are mostly behaviourally empty.** Reproducible builds would stop a cascade at
the first module whose output did not move; without them one commit redeploys the fleet for no
change in behaviour.
- **Detection stays the fragile input for latency**, though no longer for correctness.
@@ -1,64 +0,0 @@
---
topic: building it
status: accepted
date: 2026-04-03
deciders: jochen
reconstructed: true
---
# 11. Managed files are generated onto nodes and never edited there
> Reconstructed after the fact from the evidence cited below.
## Context
[ADR 0006](0006-the-substrate-and-the-control-plane.md) put every binding in the mesh
database. But the things that consume those bindings — environment files, service
definitions, daemon configuration, firewall rules — are files on a node's disk, because that
is what the software reading them requires.
So the same value exists twice: authoritatively in the database, and materialised in a file.
Any edit to the file is a change to a copy. Before this decision, environment values could be
pushed from a node back into the database, which made the direction ambiguous in both
directions at once.
## Considered options
1. **Bidirectional sync** — a node's edits flow back to the database. Rejected, and removed.
Two writers and no arbiter: whichever synced last wins, and neither is authority.
2. **Files are authoritative; the database is a cache of them.** Rejected — it inverts
ADR 0013 and returns to state that cannot be reconciled across nodes.
3. **Strictly one-directional: the database is written, files are generated.** Chosen.
## Decision
Every managed file is **derived**. A synchroniser regenerates it from the mesh database
whenever the underlying values change. The write path is the mesh tool that owns the value;
the file is an output.
This applies to generated environment files, service definitions, managed configuration, and
anything else a synchroniser lists as its own.
An edit to a managed file survives until the next synchronisation and is then overwritten,
without a warning, taking whatever it was fixing with it.
## Consequences
- **A file edited on a node is a bug with a delay on it.** This is now one of the mesh's core
values, and it is a consequence of this decision rather than a stance taken independently.
- To change a value you must know which tool owns it. That is a real cost, paid every time,
and the reason the mesh provides a way to ask whether a given file is managed.
- Debugging by editing a file no longer works, and fails in the most confusing way available:
it works, and then stops working later for no locally visible reason.
- Recovery is cheap. A node's entire managed surface can be regenerated from the database.
- Values resolve by precedence — database override, then existing file value, then generated,
then manifest default — which means an unset override does not clobber a generated
password. The subtlety is real and has caused its own confusion.
## References
- `Extract hal/env-sync module, remove syncEnvToDb`, 2026-04-03 — the commit that removed the
node-to-database direction.
- Knowledge base: `conventions/no-direct-file-mutation`, `provisioning` (resolution priority).
- Regeneration gaps: `troubleshooting/changed-manifest-default-not-rerendered`,
`troubleshooting/config-removed-from-manifest-not-pruned`.
@@ -1,101 +0,0 @@
---
topic: building it
status: accepted
date: 2026-08-23
deciders: jochen
reconstructed: false
---
# 12. The mesh creates no symlinks — a derived file is a copy
## Context
[ADR 0012](0012-the-mesh-creates-no-symlinks.md) responded to production data loss — a hand-made
link, resolved through a container engine's volume handling, pointing a mount somewhere it
should not have — by centralising linking in the installer and forbidding it everywhere else.
That narrowed the incident class. It did not close it. The hazard is not *who* made the link;
it is that a path can resolve somewhere other than where it appears to. A link made by the
installer resolves exactly the same way as a link made by hand. The rule made the mechanism
rarer and better-governed while leaving the mechanism in place.
Two things have changed since, and together they remove the argument that kept it.
**The original case for linking was staleness.** A copy of a service definition goes stale
silently while the catalogue moves on, so a link was the cheap way to guarantee the running
node reads a current definition. That argument assumes the node's copy is unmanaged.
**It is not.** [ADR 0011](0011-managed-files-are-generated-never-edited.md) established that
everything on a node's disk is derived from the mesh and regenerated when its inputs change,
and the installer already **reconciles** links rather than assuming them — repointing stale
ones, adopting real files it finds where a link belongs. Reconciling content is the same
operation as reconciling a pointer, plus a comparison.
So the mesh already has the machinery that makes a copy safe, and is using a link to solve a
problem that machinery solves better. Worse, a link is conceptually the wrong shape: it makes
the node's runtime state a *pointer into source*, which is the one thing
[ADR 0006](0006-the-substrate-and-the-control-plane.md) and ADR 0011 exist to prevent.
State is derived onto nodes; it does not reach back.
## Considered options
1. **Keep ADR 0019 as the final position** — centralised linking, forbidden elsewhere.
Rejected as the status quo. It governs the mechanism rather than removing it, and the
failure it was written for remains reachable by any code path the installer trusts.
2. **Keep links but harden them** — canonicalise before mounting, refuse a link that escapes
an expected root. Rejected: it is a check bolted onto a hazard, and it has to be correct in
every consumer, including container engines the mesh does not control.
3. **Copy, reconciled by the installer, with staleness detected rather than assumed away.**
Proposed here.
## Decision
*Accepted 2026-08-25. The position is settled; the migration is not designed. See "Open" below.*
**The mesh creates no symlinks.** A file a node needs is placed on that node as a real file,
derived from the mesh and reconciled by the installer like every other managed file
([ADR 0011](0011-managed-files-are-generated-never-edited.md)).
The prohibition in ADR 0019 stands and widens: it ceases to be "only the installer may link"
and becomes "nothing links, the installer included".
When this is accepted, ADR 0019 becomes superseded rather than edited — its reasoning is why
the rule exists at all, and the incident behind it is the reason anyone believes either record.
## Consequences
- The path-resolution hazard is removed rather than governed. There is no link for a container
engine to resolve, so the class of failure that cost production data is closed by
construction.
- A node's runtime state stops pointing into source. What a node holds is derived output, which
is what the mesh's model already says it is everywhere else.
- **Staleness becomes a real problem that must be answered, not assumed away.** This is the
cost, and it is the whole cost: today a link cannot be stale, and a copy can. The answer has
to be detection — the installer comparing what is on disk against what the mesh says should
be — and it must be loud, because a silently stale definition is exactly the failure shape
this mesh keeps producing ([ADR 0010](0010-delivery.md)).
- Reconciliation gets more expensive: comparing content rather than checking a pointer's
target, on every module, on every node.
- Disk usage rises, trivially, and is not a consideration.
- Existing links must be converted. A node mid-migration holds both forms, so reconciliation
has to handle finding a link where a file now belongs — the mirror image of the adoption it
already does.
## Open
- **How staleness is detected.** Content hash, version marker, or regeneration on every
reconcile. This is the decision that makes or breaks the change and it is not taken here.
- **Whether anything must keep a link** for reasons outside the mesh's control. If something
does, that is a finding worth recording rather than an exception worth granting quietly.
- **Migration order.** Converting a node's links is a change to how its services resolve their
own definitions, which is not a change to make everywhere at once.
Until those are answered this record stays `proposed`, and ADR 0019 remains the governing rule.
## References
- [ADR 0012](0012-the-mesh-creates-no-symlinks.md) — the incident, and the rule this widens.
- [ADR 0011](0011-managed-files-are-generated-never-edited.md) — the machinery that makes a
copy safe.
- [`03-DESIGN/00-as-is/05-runtime-and-installation.md`](../03-DESIGN/00-as-is/05-runtime-and-installation.md)
— what the installer does today, including reconciliation and adoption.
@@ -1,69 +0,0 @@
---
topic: building it
status: accepted
date: 2026-05-14
deciders: jochen
reconstructed: true
---
# 13. Schema and state changes are numbered migrations, in the same language as the code
> Reconstructed after the fact from the evidence cited below.
## Context
Modules own persistent state. That state has to change as they change, across nodes that are
at different versions, some of which have data that predates the change.
Two things were being done that do not survive contact with a second node. Schema was created
at startup, so what a table looked like depended on which version last started. And migrations
were shell scripts, so they could not use the types, connection handling or helpers the module
already had, and were not compiled or checked with it.
## Considered options
1. **Startup SQL / create-if-missing.** Rejected. It converges only for a node that started
with the newest version. A node that never restarts never migrates; a node that restarts on
an old version can undo a change.
2. **Shell migrations.** Rejected. Unchecked, untyped, and a separate dialect from the module
they belong to. Also, as later discovered, packaged differently — and therefore
occasionally not packaged at all.
3. **Numbered migrations in the module's own language, compiled with it.** Chosen.
## Decision
Every schema or state change is a numbered migration file, written in the same language as the
module and compiled with it. There is no startup schema creation and no ad-hoc statement.
Rules that come with it:
- The initial migration is **frozen** once it has run anywhere. It is never modified; a change
is a new number.
- Every statement is **idempotent** — guarded so that re-running is safe.
- A change needs **both** a baseline for a fresh installation and an incremental migration for
installations that already exist. Code referencing a column requires that the migration
creating it exists.
- Migration numbers are unique. A duplicate prefix is a defect, not a style issue.
## Consequences
- A node at any version converges to the current schema by running the migrations it has not
run.
- Migrations are checked by the same compiler as the code, and a migration that does not
compile fails the build rather than the deployment.
- The rules are enforced unevenly. Duplicate prefixes have shipped repeatedly and been fixed by
renumbering afterwards; the mesh now validates for them, which is the check this rule needed
in order to be real.
- A migration directory is a feature like any other, which means it is packaged like any other
— and when packaging is wrong, migrations silently do not ship. This has happened.
- Freezing the initial migration means a fresh installation replays the entire history. That
cost grows and nothing currently bounds it.
## References
- `fix(agents): convert workflow migrations to TypeScript` (#50), 2026-05-14.
- `feat(dev_validate): guard against duplicate migration numeric prefixes` (#379), 2026-06-26 —
the rule acquiring a check.
- Knowledge base: `migrations/schema-drift`, `noxflow/troubleshooting/migration-number-collision`.
- Packaging failures: `troubleshooting/shell-migrations-never-packaged`,
`troubleshooting/provision-migration-never-applied`.
-66
View File
@@ -1,66 +0,0 @@
---
topic: building it
status: accepted
date: 2026-06-04
deciders: jochen
reconstructed: true
---
# 14. No workspace — each module is a standalone package consuming published dependencies
> Reconstructed after the fact from the evidence cited below.
## Context
Modules depend on each other, above all on the shared library every module builds against.
A workspace was the obvious way to express that: sibling packages, resolved locally, one
install at the root.
It produced a divergence that is worth stating precisely, because it is not obvious. In
development, a workspace member importing a sibling resolves to that sibling's **local source**.
In the pipeline, each module is built alone, from a clone, without its siblings present — so
the same import resolves to the **published version**. The two environments were therefore
building different code from identical source, and the failure appeared only in the pipeline,
in a module that had not been touched.
## Considered options
1. **Keep the workspace and make the pipeline replicate it** — clone every module, build the
graph. Rejected: it makes every build a whole-repository build, which is the cost the
per-module pipeline exists to avoid, and it does not extend to modules in their own
repositories.
2. **Keep the workspace and pin siblings to published versions.** Rejected as the worst of
both: the workspace's local resolution silently overrides the pin, so the divergence
remains while looking solved.
3. **No workspace. Every module is standalone and consumes published dependencies.** Chosen.
## Decision
There is no workspace. Each module is an independent package that declares its dependencies
and consumes them from the private registry, including the mesh's own shared library.
A cross-package change is therefore two steps: publish the producer, then consume it. The
pipeline does the first on push and resolves the levels so that a module always builds against
its dependencies' freshly published versions.
## Consequences
- Development and the pipeline resolve imports identically. The divergence is gone by
construction rather than by discipline.
- A module in its own repository is not a special case. It builds exactly as a module in the
monorepo does — which is what makes [ADR 0015](0015-applications-live-in-their-own-repository.md)
cheap.
- A cross-package change costs a publish-and-consume round trip. This is the real price, paid
on every shared-library change.
- There is no repository-wide install and no repository-wide build. Anything that assumed one
broke, and one thing that assumed one has stayed broken: the end-to-end pipeline harness has
not built since this decision landed. See
[`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md).
## References
- `fix(noxflow): kill npm workspace, restore encryption inside PgAdminRepo` (#240),
2026-06-04. The reason is recorded in the root package manifest, which still carries the
note explaining why no workspace exists.
- The divergence it fixed is named there: workspace members importing each other resolved to
local unbuilt source in the pipeline.
@@ -1,65 +0,0 @@
---
topic: building it
status: accepted
date: 2026-07-10
deciders: jochen
reconstructed: true
---
# 15. Applications live in their own repository; the monorepo is for the mesh
> Reconstructed after the fact from the evidence cited below.
## Context
The module system makes adding anything to the monorepo trivial — a directory and a manifest.
That ease is the problem. Standalone applications, sites and side-projects accumulated beside
the mesh's own components, and once there they inherited the monorepo's review cadence, its
pipeline detection, and its history.
The mesh's own code and an application that merely runs on the mesh have nothing in common
except the manifest format. They change for different reasons, are reviewed by different
criteria, and have no reason to share a branch.
## Considered options
1. **Everything in the monorepo.** Rejected — it is what existed. The monorepo becomes an
inventory of one installation's applications, and every application change queues behind
mesh review.
2. **A second monorepo for applications.** Rejected: the same coupling with an extra name.
Applications have no more in common with each other than with the mesh.
3. **One repository per application, registered with the mesh as a build source.** Chosen.
## Decision
Every standalone application, site or side-project lives in its own repository, with a manifest
at the root. It registers with the mesh as a build source and is then built, provisioned,
deployed and verified by exactly the same pipeline as anything in the monorepo.
The monorepo holds the mesh: the runtime, the core modules, the delivery machinery, and the
shared infrastructure the mesh itself provisions against.
Creating an application directory in the monorepo is a convention violation, and reviewers
reject it.
## Consequences
- An application's cadence is its own. It is not reviewed as mesh code and does not queue
behind mesh work.
- The separation is safe **only because** the pipeline and provisioning are identical either
side of it — which [ADR 0014](0014-no-npm-workspace.md) is what makes true. Without
standalone packages this decision would fork the build.
- The monorepo stops being an inventory of the installation, which is a precondition for
publishing anything about it.
- Discovery gets harder: there is no single listing of everything the mesh runs, and the
registry of build sources becomes the closest thing to one.
- A module source that is not registered is silently skipped by the pipeline. The cost of
being outside the monorepo is that being forgotten is possible.
## References
- The rule is stated in the governed constitution page authored 2026-07-10, §3, as a
convention violation reviewers must reject.
- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) decision 4 extends this from *new*
applications to the modules already in the monorepo.
- Knowledge base: `troubleshooting/unregistered-module-source`.
-92
View File
@@ -1,92 +0,0 @@
---
topic: building it
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 16. The lab
*Consolidated 2026-08-28 from five records. The lab is one design and was split across five
decisions taken over three days; the reasoning is kept, the fragmentation is not.*
The environment a change is run against before it reaches real machines.
> **Still the lab, no longer the test bed — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).**
> Everything here stands. What changed is what the lab is *for*: a change is verified against the mesh
> that is running, because the faults that cost the most are faults of a mesh that already exists —
> bound consumers, containers made against an older roster, an adopted machine — and a bed is by
> construction a mesh that does not. Raising a mesh from bare is now the lab's whole job, which is the
> one thing the live mesh cannot be asked to do. 0149 also supersedes
> [ADR 0068](0068-the-lab-takes-requests.md), which extended this one and was never built.
## A node in the lab is a virtual machine
It boots a stock Linux image, runs the real install, and becomes a node. **It is not a model of
a node**, so no question arises about how good the model is — which is the whole reason for
paying the cost of virtual machines rather than containers.
The lab is driven by **incus**, and a scenario is raised from a declaration.
## A router is scenery, and is therefore a container
**Nothing under test runs on a router.** It is not a participant, holds no identity, has nothing
installed on it by the mesh, and no assertion is ever made about its internals. It exists so that
packets between machines behave the way they behave in the world.
The fidelity argument that makes a node a virtual machine does not reach it: what a router *is*
does not matter, only what it *does to traffic*. So a router is a system container, and the lab
is cheaper for it.
## A scenario declares the underlay, and only the underlay
**What a hosting provider and a home router would have provided**, before any of our software
touched the machine:
- which segments exist, and their address ranges
- which machine sits on which segment, at which address
- what NAT sits between them, and which ports are forwarded through it
- which machines are detached, and may be attached or detached during a run
**A scenario declares nothing about the overlay** — no overlay addresses, no hub, no peering, no
names, no certificates. Those are the mesh's job, and a scenario that supplied them would be
testing itself.
> A scenario provides what a hosting provider and a home router would provide, and nothing our
> software is responsible for.
## A scenario is a closed address space
Every segment materialises as its own isolated link belonging to one scenario instance. **Two
scenarios raised from the same declaration hold the same addresses and never meet**, because
nothing joins their links. The declaration therefore keeps its literal addresses and they mean
exactly what they say.
**The consequence that constrains everything else: the lab never reaches into a scenario over
IP.** It talks to a machine through the virtualisation layer's own channel — the way one would
use a console rather than the network. That is what makes two identical scenarios able to run at
once, and it is why placing anything inside a machine is a hypervisor operation rather than a
network one.
## Two scenario classes, and the first has no pipeline
| | **bootstrap** | **full** |
|---|---|---|
| contains | machines, the host binary, a pinned substrate bundle | a complete mesh: forge, coordinator, delivery, modules |
| verdict from | what the host reports about the state it reconciled | a delivery result ending in verification |
| exercises | tiers 0 and 1 | tiers 2 and above, and modules |
**The bootstrap class comes first**, because it is what develops the node host, and because a
full scenario needs tiers that do not exist yet. A lab that could only raise the larger class
would be a lab nobody could use until everything else was built.
## Consequences
- **The lab tests the real code path**, not a reimplementation of it. The network a scenario
produces is generated by the same code production runs.
- **Isolation is what makes it usable in parallel**, and it costs the ability to reach in over
IP. Everything the lab puts inside a machine — a binary, an image, a file — goes through the
hypervisor.
- **A sealed scenario cannot fetch anything**, which is a real limit rather than an inconvenience:
it is why images have to be placed and why a container runtime has to be in the base image.
@@ -1,92 +0,0 @@
---
topic: checking it
status: accepted
date: 2026-08-24
deciders: jochen
reconstructed: false
---
# 17. A test defends a decision
## Context
[`how-we-build.md`](../00-META/how-we-build.md) §5 already says that **if a document states a
rule about the mesh, it says how the rule is verified**, on the grounds that an unenforced rule
is indistinguishable from a wrong one and costs more, because people believe it.
That rule is applied to prose and to acceptance criteria. It has never been applied to
**decisions**, and it should be — a decision record states something that must be true, which
is the same kind of claim.
The gap was found by review. The lab reached 2,128 lines with 1,072 of them untested, and
**no stated rule was broken.** There is no testing posture in `how-we-build.md` at all: no
expectation, no gate, no definition of done. Every decision the lab embodies — the underlay
boundary, the closed address space, routers as scenery, waiting for usable rather than for a
call to return — was verified by hand, by running scenarios and reading output, and none of
that survives the terminal it was run in.
Which is the fault the mesh already has catalogued at scale: an end-to-end harness that has
not built since 2026-06-04, and nothing said so
([`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)). Coverage
assumed rather than checked.
## Considered options
1. **A coverage percentage.** Rejected. It measures how much code a test touched, not whether
anything important is defended, and it is satisfied by tests that assert nothing. A number
would have been met by testing the parser harder while the hypervisor integration stayed
unasserted.
2. **Test-driven development as a hard rule.** Rejected, and not because it is wrong in general.
Half the lab's implementation was discovery: that the hypervisor CLI reads a definition from
stdin and hangs, that it assigns a MAC without recording it, that a stock image's boot-time
networking flushes a static address. A test written first against undiscovered behaviour
asserts a guess.
3. **A test defends a decision.** Chosen.
## Decision
**Every decision record states something that must be true. A test asserts it.**
A decision with no test is a decision that will quietly stop being true, and nobody will find
out from a document. Concretely:
- Where a decision is about **structure or logic** — what a declaration may say, what is
refused, how a name is derived — the test is a unit test, and it is **written first**, because
the behaviour is knowable before the code.
- Where a decision is about **behaviour against a real system** — a hypervisor, a broker, a
daemon — the test runs against the real thing, and is written **alongside**, because the
behaviour is discovered rather than known.
- **Mocking the boundary is forbidden.** A test that fakes a hypervisor asserts that the fake
behaves as expected, which is the shape of test this whole effort exists to stop shipping.
- **The gate is blocking, and green is the definition of done.** A change that has not run its
tests is not finished, whatever its diff looks like.
A test names the decision it defends. Not as ceremony: it is what makes the pairing checkable,
so a decision without one can be *found* rather than noticed.
## Consequences
- The question *"which tests matter"* has an answer that is not a number. The decisions are the
list, and they are already written down.
- **A new decision costs a test.** That is the intended friction — a decision nobody will assert
is one worth reconsidering.
- Integration tests need real infrastructure and are slow. That cost is accepted: a fast test
suite that mocks the boundary would tell us nothing about the boundary, which is where every
interesting fault in this session actually was.
- Some decisions are not mechanically assertable — *the mesh brokers capabilities; nodes host;
agents think* is a shape, not a predicate. Those should say so in the record rather than being
quietly exempt, so the exemption is visible.
- Records 0001–0033 were made before this rule. They are not retroactively invalid, but each
should acquire a test or an explicit note that it cannot have one, and until then this rule
is aspirational for them — which is exactly the state §5 warns about, recorded rather than
hidden.
## References
- [`how-we-build.md`](../00-META/how-we-build.md) §5 — the rule this extends from prose to
decisions.
- [`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md) — coverage
assumed rather than checked, for two and a half months.
- The sibling HQ repository for the PAPA platform states the same boundary rule — the contract
is tested against the real system, mocking the client is forbidden, and a blocking gate is the
definition of done. This record adopts that posture and adds the decision pairing.
@@ -1,99 +0,0 @@
---
topic: checking it
status: accepted
date: 2026-08-24
deciders: jochen
reconstructed: false
extends: 0017-a-test-defends-a-decision.md
---
# 18. A picture of a system is read from the system, never from what asked for it
## Context
A scenario declaration is a file. A raised scenario is a set of machines, links and rulesets.
The two are supposed to correspond, and the entire value of the lab rests on noticing when
they do not — [ADR 0017](0017-a-test-defends-a-decision.md) says a claim nothing checks is a
claim that will quietly stop being true.
Drawing a scenario makes that concrete, and forces a choice that looks cosmetic and is not.
A diagram of a running system can be produced two ways: parse the declaration and lay it out,
or interrogate the system and lay *that* out. The first is far easier — the declaration is
already parsed, already validated, already in memory.
It is also worthless for the only question worth asking of such a picture: *is what is running
what I asked for?* A drawing built from the request and captioned **as raised** answers that
question with the request, which always agrees with itself.
This is the same fault as
[04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — a firewall key declared in
five manifests and read by no code, so a manifest appears to restrict a port and restricts
nothing. The declaration was never wrong. Nothing ever asked the system.
## Considered options
1. **Draw the declaration, and label it honestly.** Cheap, and useful for review before
anything is raised. Insufficient alone: it can never disagree with itself.
2. **Draw the system, inferring the rest from the declaration where the system is silent.**
The tempting middle. Rejected — a picture where some facts are observed and some are
assumed has no honest caption, and the assumed ones are exactly the interesting ones.
3. **Two pictures, one layout, neither borrowing from the other.** Chosen.
## Decision
**A picture captioned *as raised* reads only the running system.** It never opens the
declaration, not even for a fact the system happens not to record.
Where the hypervisor does not natively hold a fact the picture needs — whether a segment is
public, what a gateway translates, whether a machine refuses inbound — **the raise records it
on the resource** as metadata, and the picture reads it back from there.
That recording carries its own rule, which is the substance of this decision rather than an
implementation note:
> **A behavioural tag is written after the behaviour works, never when the resource is
> created.**
Written at creation, a tag restates the request in a new location and inherits none of the
authority of having happened. A failed raise deliberately leaves its wreckage standing, so a
tag written up front would let a picture of that wreckage badge translation the gateway was
never configured to do — reproducing, inside the tool built to catch the fault, exactly the
fault.
So: the gateway is tagged after its ruleset applies; the machine after the read-back proves
its firewall loaded.
Both pictures render through **one layout**, so they can be put side by side and the
difference read off directly.
## Consequences
- **It earned itself on the first comparison.** Drawn side by side, every virtual machine in
the live picture held no addresses at all. A container's interface carries the name of the
device it was configured as; a virtual machine names its own — so joining addresses to
devices by name attached every address to a container and none to a VM. Nothing failed;
a whole class of machine silently lost its addresses. The two pictures disagreed, so it was
visible in seconds. It is now joined on MAC.
- Raise does more work, and writes metadata it does not itself consume. Accepted: the cost is
a few config keys, and it is what makes a raised instance self-describing.
- A resource raised before a tag existed is missing it. The reader says so rather than filling
the gap from the declaration — an untagged link draws as unknown, not as what the file said
it should be.
- **The rule generalises past diagrams.** Anything reporting on the mesh — a status view, an
inventory, a health check — is subject to it. A report assembled from the intended state is
not a report.
- The declared picture stays, and stays useful: it is review before raising, and it is one half
of the comparison. It carries no runtime status, because it cannot know any.
- **The constitution sync is now owed.** Accepted 2026-08-25, so §5 carries the rule
unqualified and playbook [05](../00-META/process/05-constitution-sync.md) is due. Until it
runs, the mesh does not enforce this — an unsynced rule is a rule the mesh does not enforce,
whatever this document says.
## References
- [ADR 0017](0017-a-test-defends-a-decision.md) — a claim nothing checks stops being true.
- [ADR 0016](0016-the-lab.md) — why the lab must not supply what the
mesh is responsible for; the same instinct, applied to facts rather than to configuration.
- [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — the fault in production
form.
@@ -1,132 +0,0 @@
---
topic: how we work
status: accepted
date: 2026-08-28
deciders: jochen
reconstructed: false
---
# 19. How this repository works
*Consolidated 2026-08-28 from ten records that were one decision seen from ten angles. The
reasoning is kept; the fragmentation is not.*
## The repository
**`novox/hq` is Novox's headquarters, and it is public.**
Company-scoped, not the mesh's. Today almost everything in it is about the mesh, because the
mesh is what Novox is building — a fact about the present rather than a definition. A second
product would live here too.
**Public** means written for a reader who is not its author and has no access to the mesh it
describes. Nothing here may contain routable addresses, real domain names, node names, absolute
paths, usernames or credentials. The test: *would this paragraph still teach a stranger running
an entirely different mesh?*
**Separate from the code** because the cadence differs — a decision changes when thinking
changes, not when code changes — and because a public repository cannot be a private one's
subdirectory.
**The naming rule:** a repository belonging to a product carries that product's prefix. A
company-scoped one does not. So this is `hq` and the mesh's are `mesh-*`.
| Repository | Tier | Holds |
|---|---|---|
| `novox/mesh-host` | 0 | the node host — the one binary installed by hand |
| `novox/mesh-substrate` | 1 | the pinned tier-1 services, as declarations |
| `novox/mesh-control` | 2 | the control plane and its contexts |
| `novox/mesh-surfaces` | 3 | tools, web, cli — thin, no logic |
| `novox/mesh-sdk` | — | the mesh's own domain ([ADR 0009](0009-modules-and-the-graph.md)) |
| `novox/mesh-lab` | — | the lab: scenario lifecycle, networking, placement |
| `novox/hq` | — | this one |
**The product is `Novox Mesh`**, shortened to `mesh` in internal use — repository names, the
module namespace, environment variables, paths. **`Nox` is an identity of Novox**, an agent
participant within the mesh's own model, not a second system.
## The folders, and why they are numbered
**The numbering is the flow.** Research produces a decision; the decision authorises a design.
Following the numbers walks the process in the order it happens.
| | |
|---|---|
| `01-RESEARCH` | an open question, while it is open |
| `02-DECISIONS` | what was decided, and why |
| `03-DESIGN` | what is being built |
| `04-ISSUES` | something wrong at the level of design or governance |
**`03-DESIGN` has two layers and they are never mixed.** `00-as-is/` describes the mesh that
exists, written from the implementation and the operational record. `01-to-be/` describes the one
being built toward. Every document says which it is. A statement about the future does not belong
in an as-is document, and an as-is document is never edited to describe an intention.
**`04-ISSUES` is for design-level faults** — a rule enforced by nothing, a stated invariant that
is false, a failure the design permits to be silent. Not an operational ticket queue.
## What a decision record is, and is not
**If a decision is worth recording, it is worth a record. If it is not worth a record, it is not
recorded.**
That bar has been read too generously. A *finding* is not a decision. A bug is not a decision.
**A record is warranted when there is a genuine fork**: a direction reversed, an alternative
seriously considered and likely to be proposed again, or something contested that needs to stay
settled. Everything else belongs in the design document, where the reasoning is read.
**There is no ledger** — no separate document summarising, ranking or tracking decisions. A
chronological view is generated from frontmatter, which is what a ledger was actually for.
**A number identifies a record and never changes.** It is not a position, and it cannot be
both — a position moves when the set changes, and an identity that moves is not one.
That is not a preference. Records are referenced from **outside** this repository: code
comments, commit messages, the knowledge base. Renumbering once cost 96 references across two
code repositories, and nothing in either would have failed to compile — the comments would
simply have pointed at the wrong reasoning, which is worse than a broken link because nothing
reports it.
**So the reading order lives in a generated index**, from each record's `topic:` — what the mesh
is, then its tiers from the bottom up, then what runs on them and how it gets there, then how it
is built, how it is checked, and how we work.
**And the index is written, not only generated on demand.** A reader looking at the folder on a
forge sees the folder, not a command. The objection to a written index is that it drifts, and
that is answered by **checking** it rather than by refusing to write one — which is §5's own
rule: a rule states how it is checked. A record with no topic, or a topic nobody defined, fails
the same check, because the quiet failure is a record that vanishes from the order rather than
appearing in the wrong place.
**The design layer is what you read.** These records explain *why* a thing is as it is. They are
not a description of the system, and needing to read them to understand it would mean the design
documents had failed.
## Status, and views over it
**Every document carries its state in YAML frontmatter** — research overviews, design documents,
decision records, issue reports.
**There are no central status files.** Every cross-cutting view — a status matrix, a decision
index, an open-issue list — is generated from frontmatter when asked for, never written to disk.
Two places holding one fact drift, and the written one wins by being closer to hand.
**Prose does not restate status.** One place, and two is one too many.
## Workflows are playbooks
Every workflow is a playbook in [`00-META/process/`](../00-META/process/): trigger, who runs it,
steps, outputs. People and agents follow the same ones, and **agents do not act outside them**.
Each is wrapped by a thin skill that defers to the playbook as authoritative and adds only the
mechanical scaffolding — so the process has one definition rather than a document and an
implementation that disagree.
## Consequences
- **A reader has one place per thing.** The design layer describes the system; these records say
why; the playbooks say how work is done.
- **Records will accumulate more slowly**, because the bar is a fork rather than a finding. This
record is itself the correction: ten records became one because they were one decision.
- **The public rule constrains everything written here**, permanently and at every commit. It is
the reason research describes real observations without identifying the mesh it observed.
@@ -1,64 +0,0 @@
---
topic: how we work
status: accepted
date: 2026-07-10
deciders: jochen
reconstructed: true
---
# 20. The mesh is governed by a constitution, injected where work is decided
> Reconstructed after the fact from the evidence cited below.
## Context
By mid-2026 the mesh was doing a large share of its own design and implementation work through
agents. The rules those agents were expected to follow existed — in operating instructions, in
convention documents, in the knowledge base — but they were **retrieved**: an agent had to know
a rule existed in order to look it up.
Rules that must be looked up are followed by whoever already knows them, which is precisely the
population that does not need them. The rules being violated were the ones nobody thought to
search for.
## Considered options
1. **Documentation plus review.** Rejected — it is what existed. Review catches a violation
after the work is done, and only if the reviewer knows the rule.
2. **Lint and automated checks only.** Rejected as insufficient, not wrong. A check catches
what can be expressed mechanically; most of these rules are about judgement — what belongs
in a repository, when a criterion counts as verified.
3. **A canonical rule set, injected into context wherever work is decided, with a check phase
before output is accepted.** Chosen.
## Decision
A single canonical document states the mesh's non-negotiable rules. It is **injected
proactively** into every eligible design and analysis session — agents do not fetch it, it
arrives — and a check phase verifies the session's output against it before the work proceeds.
It is a governed document, not a page. Changing it requires a proposal, sign-off by reviewers
who are not the proposer, and a recorded decision. Drive-by edits are reverted.
Scoped override pages may **tighten** it for a team or product. They may never relax it.
## Consequences
- A rule reaches the work whether or not anyone remembered it existed.
- The check phase makes a violation a blocking outcome rather than a review comment.
- Two copies of the same rules now exist: this document, and the reasoning in HQ that earned
them. The enforced copy wins by default, so the reasoned copy quietly stops being true —
which is why [`00-META/how-we-build.md`](../00-META/how-we-build.md) is now the source
and the governed page is derived from it, via playbook
[`05-constitution-sync.md`](../00-META/process/05-constitution-sync.md).
- Injection costs context on every eligible turn, and grows with the document. Nothing
currently bounds that.
- The amendment process requires two reviewers, which a mesh with one human operator satisfies
only by counting agents. That tension is real and unresolved.
## References
- The governed page was authored 2026-07-10 and carries its own amendment process.
- `feat(noxflow): HAL architectural conformance gate for reviewer + architect` (#296),
2026-06-10 — the check phase, predating the document it checks against.
- Knowledge base: `platform/constitution`.
@@ -1,68 +0,0 @@
---
topic: how we work
status: accepted
date: 2026-08-23
deciders: jochen
reconstructed: false
---
# 21. HQ is the source of the mesh constitution
## Context
[ADR 0020](0020-the-mesh-is-governed-by-a-constitution.md) established a canonical rule set,
injected into every eligible design session and checked before output is accepted. It lives in
the knowledge base, where the orchestrator reads it.
HQ separately carried a document stating overlapping rules with the reasoning that earned each
one. Two texts, one enforced and one not.
That arrangement has a predictable outcome and it is not a tie. The enforced copy wins by
default, because it is the one that blocks work. The reasoned copy quietly stops being true,
and the rules survive without the incidents that justify them — at which point a rule reads as
arbitrary, and an arbitrary rule is the kind people route around.
## Considered options
1. **The knowledge-base page is the source; HQ points at it.** Rejected, though it is the
honest description of what was already happening. It leaves the reasoning downstream of the
rule, and the reasoning is the part that makes a rule survive a challenge.
2. **Accept the overlap and let both stand.** Rejected: two authorities is no authority, and
the drift is silent.
3. **HQ is the source; the governed page is derived and published from it.** Chosen.
## Decision
[`00-META/how-we-build.md`](../00-META/how-we-build.md) is the source. The governed page the
mesh injects is **derived** from it — the rules without the reasoning — and is never edited
directly.
Publishing is a playbook step, not a manual act, and it ends with **reading the page back and
verifying the change is present**. A publish that reported success and did nothing is exactly
the failure class this mesh keeps producing
([ADR 0010](0010-delivery.md)).
Section numbering is stable, because the orchestrator and the review fragments cite sections by
number.
## Consequences
- One source, many surfaces — the same argument HQ's separation already rests on
([ADR 0019](0019-how-this-repository-works.md)), applied to the rules themselves.
- Each rule keeps the incident that earned it, in a place that is reviewed as a diff.
- An edit to the derived page survives until the next sync and then vanishes. The playbook says
so, and nothing mechanically prevents it.
- **The sync is manual and is the weak point.** An unsynced rule is a rule the mesh does not
enforce, whatever the source says — so the playbook requires the failure to be stated rather
than passed over. This is the same class of gap as
[`04-ISSUES/006`](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md),
and it is worth watching for the same reason.
- The document grew from four rules to seven sections, because it now has to carry everything
the mesh enforces rather than only what someone thought to write down.
## References
- [`00-META/process/05-constitution-sync.md`](../00-META/process/05-constitution-sync.md) —
the sync, including the read-back.
- [ADR 0020](0020-the-mesh-is-governed-by-a-constitution.md) — the governed page and why it
exists.
@@ -1,105 +0,0 @@
---
topic: how we work
status: accepted
date: 2026-08-26
deciders: jochen
reconstructed: false
---
# 22. The constitution absorbs what is already enforced
## Context
[ADR 0021](0021-hq-is-the-source-of-the-constitution.md) makes this repository the source
and the knowledge base a derived copy, and playbook
[05](../00-META/process/05-constitution-sync.md) publishes the copy whenever a rule changes.
Three rules were accepted on 2026-08-25 and the sync came due. Reading the target before
overwriting it — as [§2](../00-META/how-we-build.md) requires — found the two documents had
diverged in a way nobody had recorded, and that a literal republish would have **deleted rules
the mesh currently enforces**.
| | source (`how-we-build.md`) | enforced page |
|---|---|---|
| §4 | naming and boundaries | **code quality** — structure, layering, types, restraint |
| §5 | four evidence rules | one runtime-evidence rule |
| §6 | review by a non-proposer | a design meeting with **two node operators** |
The enforced page was last written 2026-07-10, is agent-authored, and its code-quality section
appears in no decision record anywhere. It has been checked against for six weeks regardless.
Two facts made this urgent rather than tidy:
**Removal is not the safe option.** The orchestrator reads *"when absent, no constitution is
injected (backward-compatible)"* — so deleting the page would not fail, it would silently
inject nothing, and every design meeting would run unchecked with no error anywhere. Three
unenforced rules would become all of them.
**The stricter review bar has never been met.** The enforced page requires two node operators,
neither the proposer. There is one operator. Every amendment ever made violated it.
## Considered options
1. **Republish wholesale.** What the playbook literally says. Rejected — it deletes the
code-quality rules, which exist nowhere else.
2. **Merge surgically** — update §2, §3, §5 in the copy and leave §4 alone. Nothing is lost and
it is quick, but the source becomes authoritative for some sections and the copy for others,
which is the drift being fixed.
3. **Absorb, then republish.** Chosen.
## Decision
**What the mesh enforces and cannot cite is written down here first, then republished.**
The code-quality rules become §8 rather than §4, because appending renumbers nothing and every
existing citation stays valid.
**They are recorded as inherited, and marked as such.** Every other rule in the document states
the incident or measurement that earned it. These state nothing, because nothing was ever
written down. Importing them silently would have made the document claim a provenance it does
not have, and this repository's whole argument is that the reasoning is the expensive half.
**The review bar is resolved in favour of what is achievable.** A rule requiring two operators
where there is one is not a high standard, it is a rule everything silently violates — the same
shape as a firewall key declared in five manifests and read by no code
([04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)). Review means
a person who is not the proposer, which is both achievable and what has been practised.
## Consequences
- **The copy can now be a faithful projection**, which is what makes "the source is the source"
true rather than aspirational.
- **§8 has no reasoning behind it and says so.** Anyone may propose replacing an inherited rule
with an earned one; until then the marking is the honest state. It is also a standing
invitation to delete any of them that turn out to serve nothing.
- **Six weeks of drift went unnoticed** because nothing compares the two. The sync is manual and
runs only when someone remembers a rule changed. **Nothing detects divergence**, and this
record does not fix that — it is worth an issue.
- **The operator's instinct to remove the copy was right about the direction and wrong about the
method.** The end state where the mesh reads this repository directly — rendered and operable
through the board — removes the second copy rather than blanking it. That is not designed and
is not decided here.
- **Section numbering is now append-only in practice.** §8 sits after the overrides section
purely because renumbering would break citations, which is a cost of the copy existing at all.
## Sync
Run 2026-08-26, and the read-back earned its place in the playbook.
**The first publish reported success and changed nothing.** It created a new revision and
updated the title, and the body did not apply — a malformed argument was dropped silently. Had
the sync been marked done on the strength of the tool saying *"updated; new revision created"*,
three accepted rules would have gone unenforced while every record said otherwise, and nothing
would ever have contradicted it.
That is §5 demonstrating itself during its own publication: a green result proves transport,
not effect. The republish was verified by reading the rule back out of the live page, not by
trusting the second success message either.
## References
- [ADR 0021](0021-hq-is-the-source-of-the-constitution.md) — source and copy.
- [ADR 0020](0020-the-mesh-is-governed-by-a-constitution.md) — why the copy is injected at all.
- [Playbook 05](../00-META/process/05-constitution-sync.md) — the sync this record interrupts.
- [ADR 0017](0017-a-test-defends-a-decision.md), [ADR 0018](0018-a-picture-is-read-from-what-runs.md),
[ADR 0012](0012-the-mesh-creates-no-symlinks.md) — the three rules whose sync surfaced this.
@@ -1,78 +0,0 @@
---
topic: how we work
status: accepted
date: 2026-08-26
deciders: jochen
reconstructed: false
---
# 23. The approval is the checkpoint, not the second pair of hands
## Context
[§2](../00-META/how-we-build.md) reads *"one change per pull request, and never merge your
own"*, and gives the reason: *"self-merging removes the checkpoint that is the entire point."*
The rule was written for people. Applied literally to an agent it produces a contradiction
that surfaced immediately: an agent asked to merge cannot merge, because it authored what it
is being asked to merge. Every merge this repository has seen was therefore either performed
by the thing that wrote it, or not performed at all.
Stated by the operator: *"you are allowed to self-merge, after I've given you explicit
approval."*
## Considered options
1. **Read it literally.** An agent never merges; the operator merges by hand every time.
Rejected: it makes the operator a mechanical step rather than a reviewer, and a step
performed dozens of times without reading is not a checkpoint — it is the *shape* of one,
which is worse, because the record then claims a review that did not happen.
2. **Drop the rule for agents.** Rejected: the rule is right, and the failure it prevents —
work merged with nobody having looked — is not one an agent is less prone to.
3. **Require notification and approval, and stop there.** Chosen. Who performs the merge is
not the thing worth constraining.
## Decision
**Every merge into the main branch is notified and approved.** Stated by the operator in
exactly those terms, and the whole of the rule.
Notified: the merge is proposed and said out loud, not performed and mentioned. Approved: a
person says yes to *that merge*. Who then performs it does not matter, which is what makes an
agent merging its own work unremarkable — the checkpoint already happened.
**What approval is not**, because this is the half that can rot:
- A standing permission granted once and cited forever.
- An instruction to do the work, read as approval to merge it.
- Silence.
- The author's own judgement that the work is ready.
## Consequences
- **The record must show which it was.** A merge performed on approval and a merge performed
unilaterally look identical afterwards, and the difference is the whole rule. Until the forge
can record an approval, the merge commit says so — which is weaker than a recorded review and
is stated here rather than left to be discovered.
- **Branch protection would make this structural instead of textual.** Required approvals turn
the rule into something the forge enforces. It cannot be set through the API on the forge
version in use, so this remains a rule held by intention — the condition
[§5](../00-META/how-we-build.md) exists to name.
- **The one-change-per-pull-request half is untouched**, and was broken twice on 2026-08-26.
Both breaches are recorded in the pull requests themselves rather than in a conversation.
- **This narrows a rule rather than relaxing one.** The obligation moves from *who performs the
merge* to *whether a person decided*, which is a higher bar in the case the original wording
actually permits: a reviewer who merges someone else's work without reading it satisfies the
old rule and fails this one.
## Sync
Run and verified 2026-08-26. §2 in the enforced page now reads *never merge unapproved work*,
confirmed by reading the rule back out of the live page rather than by trusting the publish —
the previous sync reported success and changed nothing.
## References
- [`how-we-build.md`](../00-META/how-we-build.md) §2 — the rule, now carrying this.
- [ADR 0010](0010-delivery.md) — the standard a checkpoint is held to: a
step that reports success without doing anything is the fault, not the shortcut.
@@ -1,95 +0,0 @@
---
topic: what runs on it
status: accepted
date: 2026-08-30
deciders: jochen
reconstructed: false
---
# 24. Model access is a provision, and a licence is a thing with a name
## Context
Everything in this mesh that thinks needs a model, and there is more than one way to reach one:
| | |
|---|---|
| **hosted services** | several vendors, each with its own account, quota and key |
| **models the mesh runs itself** | open-weight models on a node with the hardware for them |
And the choice is **per consumer, deliberately**: a workstation's own session on one account, a
laptop on the organisation's, two hired workers on the mesh's local model because their work does
not justify paid tokens. Those are three different answers to one requirement, held at once, in
one mesh.
**The existing system has the hard half of this already** — automatic licence refresh and
switching between accounts when one is exhausted — and it works. It is not being replaced because
it was wrong; it is being rebuilt because it lives in a place that cannot express the rest.
## Decision
**Model access is a provision.** A module that needs to think declares `requires: model-access`;
anthropic, openai, grok and a locally-run model are four modules that provide it. That is
[ADR 0009](0009-modules-and-the-graph.md)'s mechanism unchanged, and it buys the things that
mechanism already buys: several implementations of one job, a refusal when more than one could
answer, and choosing by assigning the one you want.
**A locally-run model needs nothing new at all.** It is a mesh-scoped provision on the node with
the hardware — the same shape as a database, including the credential.
### A licence is a named thing, and the name is the operator's
Not an anonymous credential hanging off a provider. *The personal account*, *the organisation's
account* — those are names a person uses, and the mesh has to use them too, because the whole
point is saying **which one** a given consumer uses.
**Many to many.** One provider has several licences; one licence serves several consumers. So it
is **not a claim** — claims are for things only one holder may have, and two machines sharing an
account is the ordinary case rather than a collision.
### Four things this needs that the mesh does not have
Written as gaps rather than as design, because each is a real piece of work and pretending
otherwise is how a plan becomes a surprise.
**1. A provider that is not on a node.** A mesh-scoped provision today is answered by *the machine
running it*, and the reachability rule refuses two ends that share no private network. A hosted
service is on nobody's machine and is reached over the public internet. That is a third scope —
answered by a record rather than by a node — and the reachability rule must not apply to it.
**2. A secret the mesh is given rather than one it mints.** Every credential the mesh handles
today it generated itself, sealed to both ends, and discarded. An API key arrives from a person.
The missing verb is *accept*: take a value, seal it to each holder, and **discard the plaintext**
— because a mesh that keeps operator-supplied keys readably is the arrangement this project
[measured and rejected](0009-modules-and-the-graph.md).
**3. A consumer that is not a machine.** *This worker uses that licence* is a binding to an agent,
not to a node. [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) already says an agent
holds credentials and that delivery follows its node bindings and modality — so what is delivered
still lands on a machine, and what is **chosen** is chosen per agent. The provisions model has no
consumer identity other than a node.
**4. Switching is a reaction, not a declaration.** Everything here is desired state, reconciled by
comparison. A licence that hits its limit and must be swapped is a response to something observed,
and it cannot be expressed as a declaration without the declaration meaning *whatever is working
right now* — which is not a thing anybody declared. **It belongs with observability, changing a
binding**, and the binding is then declared as usual. Saying this plainly is what stops the
declaration language growing a conditional.
## Consequences
- **The refusing rule applies here and will be felt.** A mesh holding three ways to reach a model
refuses every consumer that has not said which — which is correct and is a great deal of
saying-which the first time. The remedy is one assignment per consumer, and the message names
the candidates.
- **A licence outliving its holder is a live credential nobody is watching.** The same rule the
provisioner follows applies: what the mesh granted and no longer grants is withdrawn.
- **Nothing here makes a node authenticate to a model provider.** ADR 0001 holds: an agent does.
What changes is that the mesh can now say *which agent, which licence, which node it lands on*,
which is the fact ADR 0001 records as missing.
## References
- [ADR 0009](0009-modules-and-the-graph.md) — provisions, scope, choosing, and sealed credentials
- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — agents hold credentials, not nodes;
`hal/ai` as the context owning provider grants and rotation

Some files were not shown because too many files have changed in this diff Show More