Compare 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
mesh-admin 5eadf36937 Merge pull request 'ADR 0175: the found front end is uninstalled once a machine is converged' (#292) from feat/the-found-front-end-is-uninstalled 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:29:00 +00:00
jschoubben 8c9a2c7501 ADR 0175: the found front end is uninstalled once a machine is converged; design 08 note 2026-10-02 16:27:33 +02:00
mesh-admin 54213ba90c Merge pull request 'Research 018: the operator's machine as modules' (#291) from research/018-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:27:27 +00:00
jochen 7fb59bde98 Research 018: the operator's machine as modules
The predecessor is retired on every node and what it still owned on the two
workstations — some thirty modules of dotfiles, user units and /etc files — is
owned by nothing. To-be 29 covers one directory under the home; the operator
wants the whole machine, system folders and home alike, as modules: one
default configuration each, varied per node by settings or a kept region,
never an edit; roles the machine has once as node-scoped seats with tool
contracts; the graphical stack gated by a capability so the same catalogue
serves the servers.

Four documents: the intended behaviour in the mesh's words; the predecessor's
desktop measured (34 modules, one with 88 files, 4 flavors and ~90 theme
variables) against what the records already give and what is missing (the
account is empty on every node, no user-scoped units, settings leak, tools run
in a container per module per node); the direction the operator set for where
tools run — one executor per node, host-side, module-agnostic, the console
renamed and moved out of its container, superseding 0047/0150 for tools; and
the candidate seats of the environment with first verbs, the shell first.
2026-10-02 15:19:25 +02:00
mesh-admin a4d24d7b65 Merge pull request 'ADR 0168: built and proven live' (#290) from decision/0168-built into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 13:09:22 +00:00
jschoubben 116b2d1793 ADR 0168: built and proven live — the live row read on the home server and the control node, the five rule sets removed through ADR 0170's verb 2026-10-02 15:08:58 +02:00
mesh-admin 68a14493c9 Merge pull request 'ADR 0169 → 0170: the firewall seat's record renumbered after a collision on main; built note; cycle.py refuses two records sharing a number' (#289) from decision/0170-renumbered-and-built into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 12:52:17 +00:00
jschoubben 98eb3aa76f ADR 0169 → 0170: the firewall seat's record renumbered after a collision on main; its built note; cycle.py refuses two records sharing a number
Another session's 0169 landed first. The collision check from issue 155
covered issue folders only; it covers decision records now, and would have
refused this.
2026-10-02 14:51:22 +02:00
mesh-admin 91bbe648a8 Merge pull request 'Issues 199 (resolved: a node-scoped seat's verb through the console) and 200 (the controller's answer to a long console call is refused by the bus)' (#288) from issues/199-200-console-seat-verbs-and-inbox into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 12:25:37 +00:00
jschoubben 0e7b85f184 Issues 199 (resolved: a node-scoped seat's verb through the console) and 200 (the controller's answer to a long console call is refused by the bus) 2026-10-02 14:25:03 +02:00
mesh-admin dac49de6e7 Merge pull request 'ADR 0172: the lab is a module, and runs a bed when the mesh asks' (#287) from jschoubben/the-lab-is-a-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 12:17:21 +00:00
jschoubben 0d9208dbbf ADR 0172: the lab is a module, and runs a bed when the mesh asks 2026-10-02 14:15:44 +02:00
mesh-admin bf0ee7cb25 Merge pull request 'ADR 0169: the firewall seat serves its verbs, and a foreign rule set is removed through one of them' (#285) from feat/the-firewall-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 11:29:15 +00:00
jschoubben 4567e13071 ADR 0169: the firewall seat serves its verbs, and a foreign rule set is removed through one of them
Designs 33 and 08 revised. The first node-scoped seat with verbs: rules,
reload, remove; the nftables module holds it from a runtime with NET_ADMIN,
the first container to declare a capability.
2026-10-02 13:27:03 +02:00
mesh-admin aa5d9f1045 Merge pull request 'ADR 0169 (proposed): a machine joins through the tunnel, and the bus is never public' (#284) from jschoubben/a-machine-joins-through-the-tunnel into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 11:17:42 +00:00
jschoubben 79642251a1 ADR 0169 accepted; design 08's join order starts with the tunnel 2026-10-02 13:17:32 +02:00
jschoubben 331cb94c6e ADR 0169 (proposed): a machine joins through the tunnel, and the bus is never public
The bus was public only so a new machine could enrol before it had a
tunnel. The machine now makes its tunnel key first, the token is issued
for it and makes it a peer of the hub, and enrolment happens over the
tunnel.
2026-10-02 13:13:15 +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
mesh-admin 78351560f7 Merge pull request 'Issue 198: the home network's DNS server ran outside the mesh, and its filter closed it' (#283) from jschoubben/the-lans-dns-is-the-mesh into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 10:22:51 +00:00
mesh-admin 62cc2f89c7 Merge pull request 'Issue 197: a physical link that is down is not filtered when it comes up' (#280) from jschoubben/every-physical-link-faces-outside into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 10:22:40 +00:00
jschoubben 426f741ad0 Issue 198: the home network's DNS server ran outside the mesh, and its filter closed it 2026-10-02 12:22:31 +02:00
jschoubben 9bed54d3be Issue 197 resolved: the wired port is guarded before it is plugged in 2026-10-02 12:22:15 +02:00
jschoubben 413daf8ad5 Issue 197: a physical link that is down is not filtered when it comes up 2026-10-02 12:22:15 +02:00
mesh-admin 5c993c09b7 Merge pull request 'Issues 143 and 144 resolved: the live row of ADR 0168 read on the home server and the control node' (#282) from issues/143-144-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 10:11:37 +00:00
jschoubben 7c3be48db2 Issues 143 and 144 resolved: the live row of ADR 0168 read on the home server and the control node 2026-10-02 12:11:17 +02:00
mesh-admin b665d06701 Merge pull request 'ADR 0168: a converged machine is filtered by the mesh alone, and the host says what else refuses (group 7)' (#281) from feat/one-thing-filters-a-converged-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 10:03:48 +00:00
jschoubben 1bd13446d4 ADR 0168: a converged machine is filtered by the mesh alone, and the host says what else refuses (group 7)
Designs 08 and 05 revised; 141 resolved by ADR 0140 and 084 by ADR 0102 and
issue 128, both by reading; 143 and 144 decided, built on the matching
branches in mesh-host and mesh-controller, resolved when the home server's
record names the predecessor's chain.
2026-10-02 11:58:18 +02:00
mesh-admin 14adaafa53 Merge pull request 'to-be 29: the account fact and home-scoped resources shipped; the ssh-client module, CA and ~/.ssh boundary did not' (#279) from design/29-what-shipped into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 09:49:52 +00:00
mesh-admin bd673cc6ec Merge pull request 'Group 6 closed: 086, 090, 098, 099, 100, 101 resolved on the operator's decision' (#278) from issues/group-6-closed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 09:38:11 +00:00
jschoubben bd6c55d225 Group 6 closed: 086, 090, 098, 099, 100, 101 resolved on the operator's decision, each saying the live row was not run 2026-10-02 11:37:08 +02:00
mesh-admin 2bbbc56502 Merge pull request 'Issue 194 resolved: the fixed host forgot its former archive on all four machines' (#277) from issue/194-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 09:31:49 +00:00
jschoubben c8935aceca Issue 194 resolved: the fixed host forgot its former archive on all four machines 2026-10-02 11:31:29 +02:00
mesh-admin 29b656f2c0 Merge pull request 'Issue 196: the hub relays the mesh only on the ports it publishes itself' (#276) from jschoubben/the-hub-relays-the-mesh into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 09:23:55 +00:00
jschoubben d05ac367f1 Issue 196 resolved: the hub relays the mesh, re-swept live 2026-10-02 11:23:37 +02:00
jschoubben 1c0dafb918 Issue 196: the hub relays the mesh only on the ports it publishes itself 2026-10-02 11:17:11 +02:00
mesh-admin 018ee359ae Merge pull request 'Issue 194: the host's own former archive stops every machine applying anything' (#274) from issue/194-the-hosts-own-former-archive-stops-every-apply into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 09:04:30 +00:00
mesh-admin c5535eeebd Merge pull request 'ADR 0163 built: the take digest, the minted-secret refusal, the networks setting, settings judged where stored, genesis raising the forge as declared' (#270) from feat/a-take-is-a-comparison-the-rest into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 09:04:21 +00:00
mesh-admin dbb9d2bc16 Merge pull request 'Issue 191 and ADR 0167: a membership carries what its module receives, and who the mesh is' (#273) from jschoubben/an-internal-only-route into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 07:49:27 +00:00
jschoubben 28d53dcc28 Issue 191 resolved: internal-only routes are served to the mesh, live on both proxies 2026-10-02 09:49:25 +02:00
jschoubben df667eb710 ADR 0167: a membership carries what its module receives, and who the mesh is
Issue 191's route proxy needs to know who the mesh is to serve an
internal name correctly, and the first fix had it work that out alone.
The membership on the bus now carries it, from the same list the filter
uses. ADR 0138 gains an insight that the proxy is where internal reach
is kept; designs 08 and 25 say how.
2026-10-02 09:49:19 +02:00
jschoubben 098a2ca485 Issue 191: a route with only an internal name is dropped as naming nothing 2026-10-02 09:49:19 +02:00
mesh-admin a34cedeb5d Merge pull request 'Issue 195: every assigned module is counted as a bus user without a credential' (#275) from jschoubben/issue-195 into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-02 00:44:29 +00:00
jschoubben db5ff5a5ee Issue 195: every assigned module is counted as a bus user without a credential
The status warning names 49 users; 48 are modules that never speak on the
bus, and the one real fault, a declared broker secret filled with a
generated value, looked the same as the rest.
2026-10-02 02:44:24 +02:00
jschoubben 780c2b6e58 Issue 194: the host's own former archive stops every machine applying anything
Rule 5 of ADR 0163 (a former target is removed) met issue 162 (an archive
has no removal) in the host's own archive, the first time a host carrying
former targets replaced itself; every machine applied nothing from then on.
2026-10-02 02:42:19 +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 c4151e6bc4 ADR 0163 built: the take digest, the minted-secret refusal, the networks setting, settings judged where stored, genesis raising the forge as declared; issues 096, 097, 126 resolved
The record gets its built note; designs 05 and 09 the revisions; 086, 098,
099, 100 and 101 stay located because every machine is converged and the
record's live row — a take read on an adopted machine — has not been run;
090 is built in part, its network difference left for the take to say.
2026-10-01 23:45:57 +02:00
jochen 8c231102f8 to-be 29: the account fact and home-scoped resources shipped; the ssh-client module, CA and ~/.ssh boundary did not
The controller merged §1, §2 and the composed ssh config on 2026-09-27 while hq still
called the design proposed. Record what is on main, what is held on a branch, and what
has no code — and that the account fact shipped without a decision record, which is the
next thing to write. Also drop the real account and node names the public repository must
not carry, and correct ADR numbers that drifted in a renumbering.
2026-10-01 23:14:10 +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
mesh-admin d8083bcf9e Merge pull request 'Issue 189: the plan's half is built; the moved word stays for a decision' (#269) from issue/189-the-plan-half into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 20:28:33 +00:00
jschoubben 6f26f97fdb Issue 189: the plan's half is built; the moved word stays for a decision 2026-10-01 22:25:31 +02:00
mesh-admin 63ed4a7c96 Merge pull request 'Issue 189: a rebuild from the same commit is not a move, so a packaging module's new image never rolls out' (#268) from issue/189-a-rebuild-from-the-same-commit-is-not-a-move into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 19:55:20 +00:00
jschoubben 48ca2fb41b Issue 189: a rebuild from the same commit is not a move, so a packaging module's new image never rolls out 2026-10-01 21:54:44 +02:00
mesh-admin 3976f09738 Merge pull request 'ADR 0163: taking a module over is a comparison (group 6)' (#266) from decision/0163-taking-a-module-over-is-a-comparison into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 19:13:53 +00:00
jschoubben 2904c359b8 ADR 0163: taking a module over is a comparison — what it compares, refuses and carries; designs 05 and 09; group 6's issues located, 093 resolved 2026-10-01 21:12:36 +02:00
mesh-admin b277f3b4ba Merge pull request 'ADR 0162: built, and proven live by the first tiered plan' (#265) from decision/0162-live into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 19:00:53 +00:00
jschoubben 50e4d9c2a7 ADR 0162: built, and proven live by the first tiered plan 2026-10-01 21:00:10 +02:00
mesh-admin a7d0dd83d1 Merge pull request 'Issues 184 and 188 resolved' (#264) from issues/184-188-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 18:54:28 +00:00
jschoubben a2c9fbb665 Issues 184 and 188 resolved: the merge handler returns at once; a machine is resolved with its pins and a dropped one is said 2026-10-01 20:54:10 +02:00
mesh-admin 0278766dfb Merge pull request 'Issue 188: a refusal inside "who is on the network" drops a machine silently' (#263) from issue/188-a-refusal-inside-on-the-network-drops-a-machine-silently into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 16:22:51 +00:00
jschoubben 606fbb7add Issue 188: the second fault beneath the first, and its fix 2026-10-01 18:22:19 +02:00
jschoubben a92e4bf121 Issue 188: what the live fault was and how it was resolved 2026-10-01 18:14:37 +02:00
jschoubben 187442ec7b Issue 188: a refusal inside on-the-network drops a machine silently 2026-10-01 18:10:57 +02:00
mesh-admin 47c45d4386 Merge pull request 'ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation with three kinds' (#262) from decision/0162-a-merge-produces-a-tiered-plan into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 15:54:23 +00:00
jschoubben 82496536cd ADR 0162: the three kinds of dependency, where the edges come from, and the one real cycle 2026-10-01 17:53:57 +02:00
jschoubben b0de267301 ADR 0162: the link to 0157 by its name 2026-10-01 17:46:07 +02:00
jschoubben b0a74b23fd ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation; design 30; issues 184, 186 2026-10-01 17:45:48 +02:00
mesh-admin 6a5f68d11f Merge pull request 'ADR 0084: a pin names the module as well as the node (#258)' (#261) from docs/pin-names-the-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 15:26:03 +00:00
jschoubben 821cd3b489 ADR 0084: a pin names the module as well as the node (#258) 2026-10-01 17:23:34 +02:00
mesh-admin 49b0319230 Merge pull request 'Issues 106 and 138 resolved (ADR 0161 built and live); 187 notes a report lost without retry' (#260) from issues/106-138-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 15:20:05 +00:00
jschoubben 86d1763cfa Issues 106 and 138 resolved (ADR 0161 built and live); 187 notes a report lost without retry 2026-10-01 17:18:17 +02:00
mesh-admin a7e9e6dea0 Merge pull request 'ADR 0160: the live proof, and three facts it taught' (#259) from decision/0160-live into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 14:52:04 +00:00
jschoubben ea0853ca46 ADR 0160: the live proof, and three facts it taught 2026-10-01 16:51:43 +02:00
mesh-admin fcd27c8399 Merge pull request 'Issue 184: a handler replaced mid-merge loses the rest of its work' (#257) from issue/184-also-seen into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 14:25:53 +00:00
jschoubben 03e39316ea Issue 184: a handler replaced mid-merge loses the rest of its work, and the redelivered announcement reads as history 2026-10-01 16:25:38 +02:00
mesh-admin 65a252dc00 Merge pull request 'Issue 187: the mesh tells nobody when it stops working' (#256) from issue/187-the-mesh-tells-nobody-when-it-stops-working into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 14:21:27 +00:00
jschoubben 6be284c781 Issue 187: the mesh tells nobody when it stops working 2026-10-01 16:21:05 +02:00
mesh-admin ea9a433385 Merge pull request 'Issue 186 located: the delivery dropped the asks, not the queue; a merge now rebuilds dependents; the release decision stands' (#255) from issue/186-located into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 14:19:45 +00:00
jschoubben 9ddd7215ad Issue 186 located: the delivery dropped the asks, not the queue; a merge now rebuilds dependents; the release decision stands 2026-10-01 16:16:59 +02:00
mesh-admin 626af3e8e2 Merge pull request 'Issue 186: a release across repositories is an order in a person's head, and a build is a line in a queue nobody keeps' (#254) from issue/186-the-mesh-has-no-picture-of-the-end-state-a-release-aims-for into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 14:02:04 +00:00
jschoubben 180e3b8f7e Issue 186: a release across repositories is an order in a person's head, and a build is a line in a queue nobody keeps 2026-10-01 16:01:45 +02:00
mesh-admin 4ae452d0ad Merge pull request 'ADR 0161: what deserves a seat (issues 105, 106, 138; design 26)' (#253) from decision/0161-what-deserves-a-seat into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 13:57:24 +00:00
jschoubben f35f3757bb ADR 0161: what deserves a seat — the vault's seat, the hub as a placement of capacity one, the uplink holder as the machine's dialect; design 26; issues 105, 106, 138 2026-10-01 15:55:15 +02:00
mesh-admin 6b8fb562ce Merge pull request 'Issue 185: a refused membership publish stopped the controller; 183 points to it' (#252) from fix/issue-185-a-refused-membership-stops-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-01 13:44:17 +00:00
jschoubben 2175d13935 Issue 185: a refused membership publish stopped the controller; 183 points to it 2026-10-01 15:42:37 +02:00
mesh-admin 39f0d64840 Merge pull request 'ADR 0160 built: both halves and what the first roll-out taught; issues 183 and 184' (#250) from feat/the-runtime-serves-what-it-is-issued into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 13:24:00 +00:00
jschoubben eef03f2b08 ADR 0160 built: both halves, and what the first roll-out taught; issues 183 (the controller's grant) and 184 (a merge blocks the receive loop) 2026-10-01 15:23:26 +02:00
mesh-admin 64acc94de8 Merge pull request 'ADR 0160: the mesh issues an assignment's subjects, and a runtime serves what it is issued' (#249) from decision/0160-the-mesh-issues-subjects into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 12:34:33 +00:00
jschoubben 99eb322de5 ADR 0160: the mesh issues an assignment's subjects, and a runtime serves what it is issued; designs 25, 32, 33, 34 2026-10-01 14:33:56 +02:00
mesh-admin 5460681117 Merge pull request 'ADR 0159: a tool call names the machine, every answer says which answered, and a holder's runtime serves its seat's verbs' (#248) from feat/a-tool-call-names-the-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 12:01:16 +00:00
jschoubben 0acb47fa55 ADR 0159: a tool call names the machine, every answer says which answered, a holder's runtime serves its seat's verbs; issue 182; designs 33 and 34 2026-10-01 14:00:23 +02:00
mesh-admin 7a633f2780 Merge pull request 'ADR 0158: the controller's half is built (mesh-controller 184); the provider definitions remain' (#247) from decision/0158-built into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 10:27:58 +00:00
jschoubben bef510fda2 ADR 0158: the controller's half is built (mesh-controller PR 184); the provider definitions remain 2026-10-01 12:27:39 +02:00
mesh-admin c58f4d6790 Merge pull request 'ADR 0158: a provider with one credential shares it with every consumer, and the vault remakes it for all at once' (#246) from decision/0158-a-provider-with-one-credential-shares-it into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 10:18:11 +00:00
jschoubben d29d3dfc23 ADR 0158: a provider with one credential shares it with every consumer, and the vault remakes it for all at once; designs 24 and 13 carry it 2026-10-01 12:17:49 +02:00
mesh-admin d85b41ae4d Merge pull request 'Issue 180's live proof, and issue 181 (was 163): two records answered to one number' (#245) from issue/180-live-proof into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 10:15:53 +00:00
jschoubben e00e3bc3ce Issue 181 (was 163, was 161): two records answered to 163; renumbered to the next free number across main and open pull requests 2026-10-01 12:15:31 +02:00
jschoubben b8d8101c45 Issue 180: the live rotation done — searxng's secret on the home server through the console 2026-10-01 12:14:49 +02:00
mesh-admin 85961f8348 Merge pull request 'Issue 180: a module's own secret rotates when it is read at start; the applied form stays open' (#244) from feat/231-an-own-secret-rotates into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 09:43:46 +00:00
jschoubben 48a620249b Issue 180: a module's own secret rotates when it is read at start; the applied form stays open (controller PR 183); design 13 and ADR 0114 carry the word 2026-10-01 11:43:23 +02:00
mesh-admin 7f0fe27cf2 Merge pull request 'Issue 170: assigning a module claims every seat it could hold' (#218) from issue/170-assigning-a-module-claims-the-seat-it-could-hold into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 09:24:14 +00:00
mesh-admin bfc410fafe Merge pull request 'Issue 169: a machine shares its files, and the mesh does not know' (#215) from issue/169-a-machine-shares-files-and-the-mesh-does-not-know into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 09:24:11 +00:00
mesh-admin d436122be2 Merge pull request 'Issues 164-168: found provisioning every dependency on ace' (#213) from issue/164-168-found-provisioning-ace into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 09:24:09 +00:00
mesh-admin 76a535e69e Merge pull request 'Issue 161: an assignment does not record which provider answers it' (#210) from issue/161-an-assignment-does-not-record-its-provider into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 09:24:07 +00:00
mesh-admin 027e5b8d73 Merge pull request 'Issue 179: an adopted identity provider's admin never took the secret the mesh minted' (#242) from issue/179-an-adopted-identity-providers-admin-never-took-the-minted-secret into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 09:02:37 +00:00
jschoubben 79d1619f16 Issue 179: an adopted identity provider's admin never took the minted secret (fixed by hand through the server's bootstrap; the design question left open) 2026-10-01 11:02:18 +02:00
mesh-admin 5a6b7ca4f8 Merge pull request 'Issue 178: a routed name resolves to a provider merely told it, and flips between plans' (#241) from fix/227-a-name-resolves-to-the-node-that-serves-it into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-01 00:05:14 +00:00
jschoubben e1f2c6bd5b Issue 178: a routed name resolves to a provider merely told it, and flips between plans (fixed, controller PR 181) 2026-10-01 02:04:55 +02:00
mesh-admin cf8134e318 Merge pull request 'Issue 177: the controller's check is run by nobody, and two of its tests failed for days unseen' (#240) from fix/the-converged-declaration-guard into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 23:38:36 +00:00
jschoubben 35f7f4401b Issue 177: the controller's check is run by nobody; the two rotted tests fixed (controller PR 180), the process half open 2026-10-01 01:38:22 +02:00
mesh-admin 62d61938ad Merge pull request 'Issue 176 resolved: a build is taken in where its outcome is heard' (#239) from fix/176-a-build-is-registered-where-it-is-heard into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 23:28:10 +00:00
jschoubben f841845b0d Issue 176 resolved: a build is taken in where its outcome is heard; the build tool answers with the id 2026-10-01 01:27:49 +02:00
mesh-admin 3627f7e9db Merge pull request 'ADR 0157: a build says what it does on the bus, as it happens' (#238) from feat/a-build-says-what-it-does into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 22:43:59 +00:00
jschoubben 2ffe1d0915 ADR 0157: a build says what it does on the bus, as it happens; designs 25 and 18 carry it 2026-10-01 00:43:26 +02:00
mesh-admin 763e327610 Merge pull request 'Issue 176: the console's build tool neither waits nor registers, and does not take a forge path' (#237) from issue/176-the-consoles-build-tool-neither-waits-nor-registers into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 22:29:25 +00:00
jschoubben 93f828c5eb Issue 176: the console's build tool neither waits nor registers, and does not take a forge path 2026-10-01 00:29:05 +02:00
mesh-admin fe706af63a Merge pull request 'Issues 173 and 174 resolved; the installation check refuses at registration' (#236) from feat/the-mesh-places-its-own-files into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 22:07:38 +00:00
jschoubben 52e9df0f02 Issue 153 resolved: an assignment places a module's directories and its accesses
mesh-controller PR 176 and mesh-catalog PR 198. Designs 27 and 18 carry the words: places,
accesses, ${access:<id>}, the default a definition still holds while the catalogue converts.
2026-10-01 00:04:10 +02:00
jschoubben 36454d7e4a Issues 173 and 174 resolved; the installation check refuses at registration (ADR 0155)
A setting overrides a key a contribution or served fact declares and adds none; a provider that must
tell its consumers an operator's value declares it as ${setting:…} (173). The mesh's own files for a
module are a placed directory, `place: "mesh"`, and forty-eight definitions name no host path for
them (174). Registration refuses a definition naming an installation, the day the list emptied
rather than a release later (0155, progressive insight; 134). Designs 27 and 18 carry the rules.
2026-09-30 22:36:20 +02:00
jschoubben e84c822e89 Merge pull request 'Issue 175: the link to issue 127 resolves' (#235) from fix/issue-175-link into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 20:10:41 +00:00
jschoubben a170913202 Issue 175: the link to issue 127 resolves 2026-09-30 22:10:38 +02:00
jschoubben 9a20c16d9b Merge pull request 'Issue 175: an announcement queued behind a long build came back, and the build ran again' (#234) from fix/one-announcement-at-a-time into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 19:38:49 +00:00
jschoubben 8bd0ca0bdc Issue 175: an announcement queued behind a long build came back, and the build ran again 2026-09-30 21:38:45 +02:00
jschoubben 598f6a8952 Merge pull request 'ADR 0156: an artifact is what a build produces, and the store's seat is named for its scope (group 4, step 3)' (#233) from feat/the-artifact-store-seat-is-named-for-its-scope into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
Reviewed-on: http://git.novox.be/novox/hq/pulls/233
2026-09-30 19:17:28 +00:00
jschoubben 3341c037cb Merge pull request 'Issue 119 resolved for a module's own data; issue 174 for the mesh's files (group 4, step 2)' (#232) from feat/definitions-place-their-directories into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
Reviewed-on: http://git.novox.be/novox/hq/pulls/232
2026-09-30 19:17:21 +00:00
jschoubben 22a28ad548 ADR 0156: an artifact is what a build produces, and the store's seat is named for its scope
Issue 123 resolved; glossary corrected; design 26 and ADR 0121 point at the rename.
2026-09-30 21:14:40 +02:00
jschoubben 1e1957a9c4 Issue 119 resolved for a module's own data; issue 174 for the mesh's files; design 27 phase 3 in part 2026-09-30 21:11:42 +02:00
jschoubben 5292f4176a Merge pull request 'Issue 173: a module's settings reach every fact it contributes; what the site's rename cost' (#230) from feat/group-4-step-1-closed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 19:04:36 +00:00
jschoubben 822e8b03f8 Issue 173: a module's settings reach every fact it contributes; what the site's rename cost 2026-09-30 21:04:34 +02:00
jschoubben a82941ee0c Merge pull request 'ADR 0155: a definition names no installation, how that is checked, and the three ways out (group 4, step 1)' (#226) from feat/a-definition-names-no-installation into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
Reviewed-on: http://git.novox.be/novox/hq/pulls/226
2026-09-30 18:36:42 +00:00
jschoubben 9ffb7eec55 ADR 0155: a definition names no installation, how that is checked, and the three ways out
Issues 122 and 134 resolved; design 27 in progress with its first cases; design 18 names the words.
2026-09-30 18:40:05 +02:00
jschoubben 7499f1e50c Merge pull request 'Issue 006 resolved: the record is read where it is written, and the console lists it' (#225) from feat/group-3-closed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
Reviewed-on: http://git.novox.be/novox/hq/pulls/225
2026-09-30 16:19:50 +00:00
jschoubben 214b486a50 Design 33 implemented: the mesh's verbs answer through the console, and what shipped bent 2026-09-30 18:18:44 +02:00
jschoubben 90b44a48df Issue 006 resolved: the record is read where it is written, and the console lists it
Design 35 implemented with what shipped and the live check; 006 closes on ADR 0025's own test,
run through the console.
2026-09-30 18:10:18 +02:00
jschoubben 860331dc37 Merge pull request 'ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools' (#224) from feat/the-mesh-answers-for-itself into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
Reviewed-on: http://git.novox.be/novox/hq/pulls/224
2026-09-30 15:55:18 +00:00
jschoubben 37b46d5349 ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools
Design 33 in progress against ADR 0154 (the twelve verbs, the prerequisites built); design 35 for
the records module under ADR 0153, extending 0025; as-is 07 rewritten to a mesh that keeps no store;
as-is 12 and 13 updated; issue 006 built and waiting on its live check.
2026-09-30 17:47:49 +02:00
jschoubben 16855ade02 The console shipped: design 34 implemented, as-is 13, issue 147 verified live 2026-09-30 17:13:09 +02:00
jschoubben 8dd566c0aa Merge pull request 'ADR 0152: the operator's surface is a module, the console (group 3)' (#222) from feat/the-console into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
Reviewed-on: http://git.novox.be/novox/hq/pulls/222
2026-09-30 14:47:36 +00:00
jschoubben a98ee0f529 Issues 147 and 148 name what fixed them 2026-09-30 16:21:33 +02:00
jschoubben ad4a5ea004 ADR 0152: the operator's surface is a module, the console
The work order's group-3 question answered: an ordinary module the mesh assigns to the machine a
person sits at, holding a minted credential, calling tools under a manifest grant (invokes), serving
MCP on loopback. Design 34; pointers in 33, 25 and 0095; module check designed into 12 (issue 148);
README stops claiming an indexing nothing provides (issue 006).
2026-09-30 16:08:52 +02:00
jschoubben 0cf1ad5dad Merge pull request 'Issue 172: the ssh client block matches one spelling of a machine's name' (#221) from issue/172-the-ssh-client-block-matches-one-spelling-of-a-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 13:25:34 +00:00
jschoubben 69a002fce3 Issue 172: the ssh client block matches one spelling of a machine's name
Reported by the operator: ssh by the bare name logs in, by the mesh name
is refused. The predecessor's generator writes the bare name only; the
mesh's ssh-client roster already matches both and is not yet shipped.
2026-09-30 15:25:30 +02:00
jschoubben 16a1a52cd8 Merge pull request 'Issue 171: a module that names its own resolver knows no mesh name' (#220) from issue/171-a-modules-own-resolver-knows-no-mesh-name into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 13:20:30 +00:00
jschoubben af170e3a67 Issue 171: a module that names its own resolver knows no mesh name
Found and fixed the afternoon ADR 0148 landed: mailu-admin lost its
database behind Mailu's own resolver. Two catalogue PRs; an insight on
0148 that a container's dns is a decision, not a preference.
2026-09-30 15:20:26 +02:00
jschoubben 6c2d5f5913 Merge pull request 'Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives' (#219) from issue/110-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 13:07:14 +00:00
jschoubben 04c9500b5b Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives
Issue 110's cause was not the filter: the runtime had never been told,
and the resolver dropped a query arriving on a bridge. ADR 0148 step 3
landed once it did (109, 151 resolved). ADR 0151 composes a route's
internal name under the serving node and drops the suffixed alias
(139, 157 resolved). Design 08 amended; a fact in 0148 corrected.
2026-09-30 14:56:43 +02:00
jschoubben d0044cf555 Issue 170: assigning a module claims every seat it could hold
Assigning postgres on ace claimed mesh-store and made novox's own store
assignment unresolvable. The resolver reads a manifest's claims as 'does
hold'; ADR 0110 says the assignment holds, by a deliberate act the controller
already has (seat …, HoldSeat) but resolution does not consult.
2026-09-30 14:29:11 +02:00
jschoubben 846c1f85f2 Merge pull request 'Issue 107 is resolved: a declaration carries its order' (#217) from issue/107-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 12:13:57 +00:00
jschoubben 9eef0bd525 Issue 107 is resolved: a declaration carries its order
Hosts first, then the controller — a build and a push each, now that the
mesh delivers the host. The host refuses a lower sequence than it kept
and drains a batch by sequence rather than arrival; the controller
numbers each send under the node's hold, inside the signed bytes.

Measured: two pushes, sequence 2 in the kept declaration, counters in
the store agree, no machine reads as behind. That last one is the
subtlety: the mesh compares the digest of what it would send against
what it did, and a number changes the bytes, so the read-only comparison
composes with the last number sent rather than a fresh one.
2026-09-30 14:13:50 +02:00
jschoubben 105ae9a56a Issue 169: network-share is the module responsible for a node's network shares — a node role 2026-09-30 13:56:24 +02:00
jschoubben ee801a6441 Issue 169: the consumer half is a module (network-share), not a host resource kind; the access-on-a-mountpoint check is the data-loss case 2026-09-30 13:56:07 +02:00
jschoubben 90b89aa1c9 Issue 169: the consumer's half — the host mounts the share, a mount is a resource of the consuming module, not a client module 2026-09-30 13:53:32 +02:00
jschoubben e33191161d Issue 169: two module-defined seats, and the gap — a seat definition has no neutral home
nfs-share and smb-share share an intent, not a contract; one seat would be
a union with every field optional. What the exemplar exposes is that a
seat declared inside one module cannot be implemented by another without
depending on it.
2026-09-30 13:52:20 +02:00
jschoubben 6e08cdf3d6 Merge pull request 'Every machine self-updates, verified, and 107's gate has opened' (#216) from issue/142-self-update-on-every-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 11:51:53 +00:00
jschoubben 02f291a129 Every machine self-updates, verified, and 107's gate has opened
All four machines run a host the mesh built, published and delivered, the
last delivery unattended: each stood aside once for a genuinely newer
version and the delivered launcher started it. A following push that
delivered nothing new was applied and reported by every machine and stood
nobody aside.

The crossover needs one restart of the unit per machine, once, because
the running launcher executes from its own inode. Measured timing: three
seconds on the machine, 17-20 as the operator sees it, the difference
being the control plane composing before it sends.

107 is unblocked: a declaration field is now a build and a push.
2026-09-30 13:51:46 +02:00
jschoubben a0a930b1cd Issue 169: file sharing is a core seat, one per protocol
The operator's call: a node-scoped system seat family (node-nfs-share,
node-smb-share, …) defined by the control plane, one per protocol as
package registries are (ADR 0109), so several modules occupy it and a
machine may hold both.
2026-09-30 13:50:31 +02:00
jschoubben 8d9c9ab6b5 Issue 169: a machine shares its files, and the mesh does not know
ace exports the operator's media library over NFS and Samba with host
services no module declares: they close at converge, nothing owns their
configuration, and no module elsewhere can require the share. Proposes a
file-sharing module that accesses the paths, holds a node-scoped seat it
defines, and provides file-share to consumers.
2026-09-30 13:49:25 +02:00
jschoubben 9b14430d3f Merge pull request 'Issue 163: a delivered host stood aside on every push and reported nothing' (#214) from issue/163-a-delivered-host-stands-aside-on-every-push into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 11:47:35 +00:00
jschoubben a4384f13d3 Issue 163: a delivered host stood aside on every push and reported nothing
Asked whether a newer host was delivered using the link-time stamp, which
every delivered host carries as 'development build' now that the version
comes from where the binary sits. Never matched, so it stood aside on
every push for ever; standing aside cancels the report, so the mesh never
heard from it. Read as healthy throughout.

The three-minute push wait made it invisible: a wait long enough to
absorb a whole apply is long enough to hide that the machine never
answered.
2026-09-30 13:47:28 +02:00
jschoubben 49b1136ded Issues 164-168: found provisioning every dependency on ace
164 a credential that must be accepted is minted anyway
165 one accepted value must be accepted once per consumer
166 a requirement cannot be optional
167 code several modules share has no home
168 a setting reaches every file and every contribution
2026-09-30 13:28:53 +02:00
jschoubben 743051efe7 Issue 163 (was 161): another record took 161 on main first 2026-09-30 13:28:22 +02:00
jschoubben e8470057aa Merge remote-tracking branch 'origin/main' into issue/161-an-assignment-does-not-record-its-provider 2026-09-30 13:28:22 +02:00
jschoubben a841e2c173 Merge pull request 'The host self-updates, and an archive cannot be undeclared' (#212) from issue/161-resolved-and-162-an-archive-cannot-be-removed into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 11:16:44 +00:00
jschoubben 3c535ead31 The host self-updates, and an archive cannot be undeclared
161 resolved and verified on a machine: the workstation runs a host the
mesh compiled, published, delivered and started, applying declarations
and reporting the version it was delivered as.

The system it was built for comes from the artifact — the one thing a
toolchain takes from a module, which 0142 already allowed because the
target is a property of the artifact. The version comes from where the
binary sits, which 0142 decided and nothing had implemented.

Two mistakes on the way, both caught by reading the output rather than
the line that claimed success. A second -ldflags does not merge with the
first: the binary gained its system and lost -s -w, 12.2MB against 8.5MB.
And the delivered binary was named after its package, so the first
delivery was correct, reported success and was invisible to the launcher.

A delivered host that cannot apply is a machine the mesh cannot repair,
because the declaration that would fix it is the one it cannot apply. The
launcher's fallback is what made that an inconvenience instead of an
expedition.

162 is new and not about the host: an archive has no removal, so a module
using one can never be unassigned, and the attempt takes the whole apply
with it — the machine applies nothing else either. It is how undoing the
first delivery froze the workstation.
2026-09-30 13:16:37 +02:00
jschoubben 4cf941d858 Merge pull request 'Self-update works, and a delivered host is one fact short of usable' (#211) from issue/161-a-delivered-host-has-no-link-time-facts into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 10:28:08 +00:00
jschoubben 5042ffd8d3 Self-update works, and a delivered host is one fact short of usable
The loop closed on the workstation: the version landed, the launcher was
replaced, the running host stood aside, and after one restart the launcher
started a binary the mesh had compiled, published and delivered.

The launcher goes as a file resource rather than inside the archive, and
that is the safety rather than a preference. A file is written atomically,
so the running launcher keeps the inode it started from; an archive writes
in place with truncate and would cut a script a shell is reading. The
manifest carries a second copy and a test refuses any drift from the one
in packaging.

Then it would have refused the first declaration it was asked to apply.
The Makefile links in two facts the mesh's toolchain does not, on purpose,
and one of them is the system the host was built for — read before
anything is applied, so the failure is safe and total. Nothing reports it:
the unit is active, the bus link is up, and the log says it is hearing
what the node should be.

Worse, the declaration that would fix it is the declaration it cannot
apply, so the mesh cannot repair such a machine. Restored by moving the
delivered versions aside and letting the launcher fall back, which is the
fallback working as designed.

0142 already settles the version — it comes from where the component sits,
not from its linker — and that is unimplemented. The system pin has no
answer, and the candidates are a decision rather than a fix: put it in the
path too, carry it in a file beside the binary, or stop pinning at link
time at all, which is 0005's to change.
2026-09-30 12:28:01 +02:00
jschoubben 39340fcd76 Issue 161: an assignment does not record which provider answers it
ADR 0110 decided each assignment records where its requirements are answered
from; the control plane keeps only a per-machine pin (none recorded) and
resolves every requirement implicitly. Harmless with one provider; a second
one silently moves consumers' data.
2026-09-30 11:54:31 +02:00
jschoubben 4f9dc3906e Merge pull request 'A machine says little about itself, and only when the mesh asks it something' (#209) from issue/160-what-a-machine-says-about-itself into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 08:36:48 +00:00
jschoubben a2542e51f8 A machine says little about itself, and only when the mesh asks it something
Filed as a to-do. Nothing is broken by it: every machine here is amd64 and
reports so, and one architecture is enough for now.

When a machine joins, the mesh should collect what it reasonably can about
it and refresh that daily. It already asks what a machine can do; what it
is made of is the same question one level down.

More is already collected than it looks — eight capabilities, the links
that face outside, the host version, and on an adopted machine what it
holds, what is reachable and the firewall and tunnel it was found with.
The architecture and the kernel are in there too, and nothing reads
either: measured, all four machines report amd64 and linux, and node show
prints the capabilities beside them without printing them.

Missing: memory, disk, the processor beyond its architecture, the
distribution and its version, virtual or physical, cores, uptime. Several
are what somebody wants when deciding where a module goes, and the
placement code's own comment already imagines them.

Also missing: the refresh. A machine publishes after an apply, and the
five-minute reconcile publishes nothing, so the mesh's picture is as old
as the last push. Same mechanism 087 wanted.

This is the third thing in one day found to be collected and read nowhere,
after held resources and the host version. Whatever gets added should say
in the same breath which surface shows it, or it will be the fourth.

159 gains the note that the architecture is already reported, so matching
an artifact to a machine needs no new fact — only the comparison and a
compiler told what to target.
2026-09-30 10:36:40 +02:00
jschoubben b19b29cd3a Merge pull request 'An artifact's system is checked and then nothing uses it' (#208) from issue/159-an-artifacts-system-is-checked-and-ignored into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 08:30:52 +00:00
jschoubben 4bf4fe2d06 An artifact's system is checked and then nothing uses it
Asked whether the host is built for more than one architecture. It is
not, and the reason is worse than a missing feature.

A bundle in a compiled language must name a system, must name one of
alpine, android or arch, and is refused with a careful message if it gets
that wrong. The field is then read by nothing: it does not reach the
compiler, no machine is matched against it, and nothing chooses between
two artifacts by it. The compile runs with no target named and produces a
binary for whatever the build machine happens to be.

The host is x86-64 because the build machine is, not because the
declaration said so. Correct for this mesh by coincidence — four machines,
all x86-64 Arch.

A module declaring two systems would get two identical binaries, both
published and both pinned, and the one sent to the machine it was not
built for would fail at exec. android is the sharp end: not an x86-64
platform, and an artifact declared for it today would be an x86-64 binary
wearing the label.

A field that is checked and ignored is worse than one that does not
exist, because the check is what persuades you it works.

Also noted: the processor is a second dimension the manifest has no word
for, so even implementing the present field would not answer the question
that found this. And since the Go toolchain builds statically, one binary
would run on all three systems anyway — so the pin is a policy rather
than a necessity, which is a decision and not a fix.
2026-09-30 10:30:45 +02:00
jschoubben 5e12081774 Merge pull request 'Issue 142: the mesh compiles its own host and publishes it to its registry' (#207) from issue/142-the-mesh-can-build-its-own-host into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 08:05:12 +00:00
jschoubben 9ad64881a9 Issue 142: the mesh compiles its own host and publishes it to its registry
Both of the things ADR 0141's insight named as remaining are built. A Go
toolchain based on a new mesh-tools-go module, so the compiler is named
and not pinned; and ${version} in any value of a resource that uses an
archive or a bundle.

Measured rather than asserted: the mesh built the host through its own
toolchain, published it to its own registry, and the bundle fetched back
out is a statically linked stripped binary that runs and says it is the
host.

The cost was larger again than 0141's note said. Three more things in the
path assumed one language or one shape — an entrypoint became a .ts file
whatever the language, the output directory was the compiler's to create,
and a bundle was refused if it named what it is built from — and a fourth
was in the base image, which is Alpine where the first Dockerfile ran
apt-get. That last one is issue 136 in an image, and the build refused
rather than a module failing later.

The version in a path is the digest, not the commit: two builds of one
commit are the same bytes, so a content-addressed version keeps the path
an unchanged build already had.

Still nothing delivers a version to a machine. The host module declares no
resources, so the bundle sits in the registry and no machine is asked to
take it. 0141 carries the insight and 142 the account.
2026-09-30 10:05:04 +02:00
jschoubben 266ade6e28 Merge pull request 'Issue 087: what I shipped first said the opposite of the truth' (#206) from issue/087-a-commit-has-no-order into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 07:29:53 +00:00
jschoubben a794ef9a3f Issue 087: what I shipped first said the opposite of the truth
It reported "N machines run an older host than another" by comparing
versions as strings. A host reports its version as a commit, and commits
have no order. On the live mesh it named the three machines running the
NEWER host as the ones behind — ced54d4 sorts above 04a27ca and that is
all it means.

The code even carried a caveat saying versions compare as strings and that
this "is enough for the timestamps and commits this mesh uses". That was
the error, written down and not noticed: enough for timestamps, meaningless
for commits, and the mesh reports commits.

It now reports the split and claims no ordering, which is more useful as
well as more honest — the reader sees who is on which side, and that is
what decides whether a field can be sent. Ordering is left with the host,
which would have to report something ordered for anybody to have it.

This is issue 145 arriving by my own door an hour after I closed it: a
report that confidently says the opposite of the truth is worse than one
that says less.
2026-09-30 09:29:40 +02:00
jschoubben e10084fed6 Merge pull request 'Issue 087: a machine states its host only when the mesh sends it something' (#205) from issue/087-a-machine-says-its-host-only-when-asked into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 07:09:17 +00:00
jschoubben 941b920bd7 Issue 087: a machine states its host only when the mesh sends it something
Measured live. All four machines run the identical host binary — same
digest, installed within eighteen seconds — and at first only one reported
a version, which read as a difference where there was none.

A machine publishes a report after an apply. The five-minute reconcile
publishes nothing, because it is the machine keeping itself as declared
rather than answering anything. So a current, idle machine never says, and
the mesh cannot tell that from a machine running something ancient.
Confirmed by pushing: not reported, then 04a27ca.

Enough for the purpose, not enough for the claim. For deciding whether a
new declaration field is safe it is sufficient — pushing is what the mesh
is about to do, and the answer arrives with the act. For knowing what the
mesh runs it is not, and "not reported" is worded as "nobody has asked
recently" for that reason.

Making a heartbeat carry it would close the gap and would change what a
heartbeat is — a bare word that the node is there, deliberately carrying
nothing else. Left alone rather than widened in passing.
2026-09-30 09:09:10 +02:00
jschoubben ecbd2ce2e6 Merge pull request 'Group 1: 145's report states its scope, and 107 waits for delivery' (#204) from issue/145-and-107-what-group-one-leaves into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 07:00:32 +00:00
jschoubben b7f7b97d8a Group 1: 145's report states its scope, and 107 waits for delivery
145, partly resolved. The sentence that was true for eleven hours of a
mesh in which no module could reach another now says what it is not a
claim about: that is the mesh and the machines agreeing, and nothing here
dials a provision. It checks nothing and does not pretend to — ADR 0146
decides the check and is deliberately not built. What changed is that the
report no longer implies otherwise. Stays open for that reason.

Carried forward: 0146's check needs an internal name fetched over TLS with
the certificate verified, and until today no machine trusted the mesh's
authority. Three of four do now, so whoever builds it does not have to
solve that first.

107, diagnosed and deliberately not built. The premise is confirmed in the
host's own words — unknown fields are refused because "a field the host
does not know is a thing the control plane believes it asked for" — so the
fix is a flag day, not an addition. 087 now makes the cost measurable, and
the measurement is why it waits: one machine of four runs an older host,
it is parked, and nothing delivers a host at all (142). Shipping the field
means hand-placing binaries and unparking a machine, and one missed in
that sequence is unreachable, not degraded. The fault it prevents has
never been observed.

142 gains the note that it is 107's gate, and that it is what makes a
declaration field cost a rollout instead of an expedition.

A judgement about order, not a refusal, and cheap to overrule.
2026-09-30 09:00:18 +02:00
jschoubben c2cf0d72d6 Merge pull request 'Issue 087 is resolved: the mesh knows which host runs a machine' (#203) from issue/087-the-mesh-knows-which-host-runs-a-machine into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 06:55:05 +00:00
jschoubben 0a5006b366 Issue 087 is resolved: the mesh knows which host runs a machine
The machine has reported its host version since ADR 0141, whose own
comment says why it must: without it nothing can say a machine is behind.
The controller's copy of the report did not have the field, so it
unmarshalled into nothing and was thrown away on arrival. Two structs
describe one message and only the sending side had it.

node show names it per machine, "not reported" where the mesh has not been
told. status names every machine running an older host than another does,
and which is newest.

Disagreement rather than staleness, deliberately: nothing delivers a host
version yet, so the mesh holds no canonical current one and "behind" has
no fixed point. What it can say is that the oldest host in the mesh is
what the mesh may send.

Two refusals to guess: a machine that reported nothing is not called
behind, and versions compare as strings — right for the timestamps this
mesh uses, wrong for a scheme where 10 sorts before 9, said at the place
that would have to learn.

107 gains the note that this is what makes its new field safe to consider,
and that one machine of four is behind today, so it is not free yet.
2026-09-30 08:54:52 +02:00
jschoubben 3ea9601904 Merge pull request 'Issue 125 is resolved: a hold is a line in the report, and it breaks "all well"' (#202) from issue/125-a-hold-is-a-line-in-the-report into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 06:47:57 +00:00
jschoubben f7a37ee4f3 Issue 125 is resolved: a hold is a line in the report, and it breaks "all well"
Two of the four surfaces the report named already carried it — the host
has reported Held since ADR 0100, and node show reads the machine's own
list with an `as of` beside it. Recorded as checked rather than assumed.

Two did not. The apply line counted what it applied and said nothing
about the difference; status read the mesh's take-time listing, so a
module assigned after it showed nothing at all.

Both now say it, and the part that carries the weight: a hold suppresses
"all doing what they were told, all heard from, running what the mesh
would send them". That sentence was true for the whole outage, and acting
on it is what stopped the predecessor's proxy. Being adopted still does
not suppress it — a mode somebody chose is not a half-finished action.

Status does not call a hold a fault, deliberately. It is correct
behaviour, and a reader trained to see red for something the mesh did
right stops reading.
2026-09-30 08:47:36 +02:00
jschoubben 601d004fcb Merge pull request 'Issue 129: three machines of four trust the mesh, not one' (#201) from issue/129-three-machines-not-one into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 00:30:30 +00:00
jschoubben d009c3efef Issue 129: three machines of four trust the mesh, not one
Extended after the first was proven. Every converged machine now holds
the anchor and verifies an internal name with a plain client; before,
the two unassigned ones answered 'unable to get local issuer
certificate' and held no entry for the mesh.

ace is excluded on purpose: it is adopted, so a module assigned there is
held rather than run, which is right and is not trust.

Both the resolution and 0147's insight said one machine of four, which
was true for about twenty minutes.
2026-09-30 02:30:23 +02:00
jschoubben 5036b927b9 Merge pull request 'Issue 129 is resolved: a workstation trusts the mesh, and stops when told to' (#200) from issue/129-a-machine-trusts-the-mesh into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-30 00:18:51 +00:00
jschoubben 1f72e82b84 Issue 129 is resolved: a workstation trusts the mesh, and stops when told to
Registered ca-trust from the catalogue — it was merged and had never been
registered, which is why "assign it to one machine" had no module to name
— assigned it to the workstation, and verified.

Verified in the form ADR 0147 prescribes, against the authority's own API
so the handshake needs nothing else in the mesh to be right: 200, issuer
Mesh Internal CA, Verify return code 0. Four routed internal names verify
too, and `git ls-remote https://…` works, which is the consequence the
report named.

Removal exercised for the first time. Unassign and push removes the
anchor, empties the trust store of the mesh's authority, and returns the
plain client to the original error; assigning again restores it. That is
the half 0147 claimed and nothing had shown.

One thing it found that is not in the module: removal works only because
the host removes the service before the script. Stopping the unit is what
deletes the certificate and refreshes the bundles, and it needs the script
to still exist. The symmetry rests on an ordering nothing states.

0147's "written, and not yet run" now carries a progressive insight
saying it has run, where, and that it ran on one machine of four.
2026-09-30 02:18:21 +02:00
jschoubben 76fbe323ea Merge pull request 'Issue 118 is resolved: it was issue 135, and umami is healthy' (#199) from issue/118-is-135-and-is-resolved into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-29 23:54:39 +00:00
jschoubben 295bf2f3ab Issue 118 is resolved: it was issue 135, and umami is healthy
Verified on the machine: umami has zero restarts, applies all 26
migrations, reports the store up to date, and umami.novox.be answers 200
where it had answered 502 since 2026-09-25.

It was the same fault as 135, filed three days earlier and diagnosed
without either record noticing the other. 135's container IS umami — it
is named in 135's own evidence table, holding novox.internal:10.42.0.1
after the overlay range moved. The dial appeared to succeed and the first
real query timed out because the name pointed at an address that no
longer existed; the store, the path and the credential were all fine.

118's own reasoning is kept as a warning, because it is careful and
wrong: it argued path-MTU and conntrack, and it named the move that would
have found it — compare umami against a container created that week —
and did not make it. A stale name presents as a network fault, which is
why ADR 0148 stops copying names into containers rather than detecting
when the copies go bad.

Also: 118's located-in named mesh-catalog modules/umami, which was never
at fault; it names mesh-host's comparison now. And 135 gains the pointer
to 118, which is the back-reference I have now missed three times.
2026-09-30 01:54:19 +02:00
jschoubben e1b74810a8 Merge pull request 'Issue 129 is live and reproduced, and needs three steps rather than one' (#198) from issue/129-and-what-reproducing-it-found into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-09-29 23:30:48 +00:00
jschoubben e41eed0852 Issue 129 is live and reproduced, and needs three steps rather than one
The certificate is genuine, from Mesh Internal CA, and nothing on the
workstation trusts it — verbatim the error the report gives. The public
name on the same proxy verifies cleanly, which puts the fault exactly
where the report puts it.

What is in the way is not an assignment. `ca-trust` is merged in the
catalogue and has never been registered with the mesh — 39 of 76
manifests are — so there is no module to assign. It dry-runs clean and
needs no artifact built.

Two findings from reproducing it, both their own issues:

157 — every routed name is published with an `.internal` alias that
nothing serves. The hosts file says keycloak.novox.be.internal; the proxy
serves keycloak.novox.internal and refuses the other by name. The first
three names I tried came from the hosts file and failed with a TLS alert
rather than a verification error, which pointed at a regression that had
not happened.

158 — the proxy re-logs all 52 routes every two seconds, 31 times a
minute. The one line that explained 157 sat between two of them.

Also recorded, because it was nearly filed as a defect and is not one:
step-ca publishes roots as /roots.pem, which is PEM, so ca-trust's fetch
and its refuse-a-non-certificate guard are both right. Its other endpoint
/roots returns JSON that contains the text the guard greps for, so the
guard is sound only because of which path is published.
2026-09-30 01:30:24 +02:00
jschoubben ba30286896 Merge pull request 'The pointers back from what yesterday's records changed, which I missed twice' (#197) from decision/the-pointers-back-from-what-these-narrow into main 2026-09-29 22:47:45 +00:00
jschoubben 53b94c51bb The pointers back from what yesterday's records changed, which I missed twice
Three records were left describing a mechanism a new record had moved,
and a reader arrives at them by following a citation: 0066 still said a
routed name is written into every container after 0148 replaced that with
resolution; 0016 still read as though the lab were the test bed after
0149; and issues 109 and 135 said nothing about 0148 ending the copying
that 135's own fix made comparable. Each was a citation leading to the
wrong answer in a record that was not wrong about anything it decided.

This is the second time in one session. The playbook rule I added last
round did not stop it, so the convention is now written where the record
conventions live, with the shape to use and three worked examples — and
with the honest note that it is NOT machine-checked and cannot be from
`extends:` alone: 102 records extend another, 87 have no back-reference,
and that is correct, because extending usually means building on a
context. Making it mechanical means a record declaring the relationship
in frontmatter, which is a schema change and is not mine to decide.

Also: designs 18 and 20 claimed `updated:` dates from before I edited
them, and 117's `fixed-by` gained the commit beside the record.
2026-09-30 00:47:19 +02:00
jschoubben 83791f0921 Merge pull request 'The four open design questions, answered: ADRs 0148, 0149, 0150, and 0114 accepted' (#196) from decision/0148-the-meshs-names-are-resolved-not-copied into main 2026-09-29 22:39:38 +00:00
jschoubben bf4d4e2e7b The three parked questions are answered: 0114 accepted, 0068 superseded, 117 settled
**0114 accepted.** The separation it draws — a consumer's resource and a
credential that reaches it are different things — is a data-loss rule,
and a record naming one should not sit unresolved while the code that
could hit it is written. Not built, and accepting it schedules nothing;
accepted-and-not-built is where 0141 and 0142 already are.

**0068 superseded by 0149, the live mesh is the test bed.** Never built,
and contradicted by practice that was written down nowhere but a handoff
note. 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.
Issue 156 settles it: that change was verified honestly, on the only path
where it cannot fail. The lab is not retired; raising a mesh from bare is
now its whole job.

**117 answered by 0150.** A module's own code runs as supervised
processes under the module's one account. 0047's argument was for a
runtime per module, not for a container — a unit satisfies it and runs as
an account. The invariant is the account, not the process count: its
worry was a second identity to scope and seal, and processes sharing one
account create none. Designs 18 and 20 now cite it and 0047 carries a
dated pointer, which closes the third disagreement — that neither design
knew the record existed.
2026-09-30 00:39:09 +02:00
jschoubben ec42ee0846 ADR 0148: the mesh's names are resolved, not copied into every container
Answers issue 151. Copying the roster into each container made the roster
part of each container's identity, so one name moving replaced every
container in the mesh — and it never stopped the staleness it was for,
since a copy taken at creation is stale the moment the roster moves (109,
135).

A container resolves through its machine's resolver instead, and nothing
is copied. Staleness stops being possible rather than detected, and a
name's blast radius becomes nothing.

Scoping each container to the names it binds was the close call and is
rejected: it contradicts anything-calls-anything, and leaves the roster
in the digest so the churn returns for a widely-bound name.

Gated on issue 110 — a container on the runtime's default network has no
DNS at all today. Removing the copy first reintroduces 109 and 135
silently on a live mesh. 151 stays open until the code lands; design 08's
file-not-resolver passage is narrowed to the machine's own roster.
2026-09-30 00:34:30 +02:00
jschoubben 5a3dee9e9e Merge pull request 'The records pointed at branches that no longer exist, and two fixes had no sequel' (#195) from issue/pointers-that-resolve-to-nothing into main 2026-09-29 22:29:02 +00:00
jschoubben 47909c6b71 The records pointed at branches that no longer exist, and two fixes had no sequel
Three issues named the branch that fixed them, and a branch is deleted
when it merges — so every `fixed-by:` was a pointer that resolved to
nothing by the time anyone followed it. They name commits and pull
requests now, and playbook 03 says to.

Two records were missing the thing a reader arrives for. 146 did not say
that one of its fixes crash-looped the control plane on a running mesh,
which is the whole reason the delivery subject carries the stream and the
raise path was the only one exercised. 151 did not say that 152 removed
the false reasons its roster moved, or that it stays open for the real
ones.

ADR 0080 enumerates what cycle.py enforces and named four things; it
enforces five. A progressive insight names the fifth — the decision
stands, the list had gone stale. The checks README and playbook 03 gained
the same rule, and 155 points at all three.
2026-09-30 00:28:35 +02:00
jschoubben 7e5edab8da Merge pull request 'Issue 156: the wider grant has landed, and the notes the fix printed' (#194) from issue/156-the-grant-has-since-landed into main 2026-09-29 22:19:35 +00:00
jschoubben 3649f82204 Issue 156: the wider grant has landed, and the notes it printed
Measured on the broker's own config after the fixed controller composed
it: every node is now allowed _DELIVER.<node>.> as well as the bare
subject. That grant was missing when this was diagnosed, which is why
re-making the consumers would have silenced every machine.

Also records the five consumers it kept and reported, including the build
machine's worker, which is bound the same way and was not anticipated.
2026-09-30 00:18:57 +02:00
jschoubben b400ce2c54 Merge pull request 'Issue 156: moving a consumer's delivery subject stops a running mesh' (#193) from issue/156-a-consumer-that-works-is-not-replaced into main 2026-09-29 21:57:00 +00:00
jschoubben 995c8cb266 Issue 156: moving a consumer's delivery subject stops a running mesh
146's change is correct on a mesh being raised and fatal on one that is
running: the server will not move a push consumer's delivery subject
while a subscriber is bound, and a node is bound to its declaration
consumer the whole time it is up. Reproduced before fixing; an earlier
version of the test unsubscribed first and passed against the broken
code.

The consumer that works is kept and the assertion says so. Not re-made:
the nodes are granted the bare subject only, so re-making would have
silenced every machine.
2026-09-29 23:56:31 +02:00
jschoubben f89aef992d Merge pull request 'Two records shared a number, twice, and every check passed' (#192) from issue/two-records-share-a-number-and-nothing-says-so into main 2026-09-29 21:39:09 +00:00
jschoubben b967ef7be3 Two records shared a number, twice, and every check passed
Two machines filing issues in the same hour both read main correctly and
both took "the next free number". main lags every open pull request —
seven that evening — so they collided twice. The second collision
reached main with records, cycle and index all reporting success.

cycle.py now refuses a tree where two issue folders share a leading
number, and names both. Proven by adding a duplicate and watching it
fail. The colliding records become 153 and 154, renumbered in the branch
that lands last, because renumbering a branch whose author is still
pushing only moves the race.

The check catches a collision; it does not prevent one. Taking a number
still means reading the open pull requests as well as main — issue 155
says so.
2026-09-29 23:38:48 +02:00
jschoubben e417906241 Merge pull request 'Issue 150: a machine's own network is not a reach' (#190) from issue/150-a-machines-own-network-is-not-a-reach into main 2026-09-29 21:37:41 +00:00
jschoubben b1bf895688 Merge pull request 'Issue 149: an adopted machine's data cannot be placed where it is' (#189) from issue/149-adopted-data-cannot-be-placed-where-it-is into main 2026-09-29 21:37:34 +00:00
jschoubben 0d64677c70 Merge pull request 'Issue 152: a node the mesh could not read withdrew its names from every machine' (#191) from fix/152-a-lookup-failure-is-not-an-absence into main 2026-09-29 21:37:16 +00:00
jschoubben 04205c1dc8 Merge pull request 'Issues 147 and 148: a route before its module is taken; a new name recreates every container' (#188) from issue/147-148-found-migrating-ace into main 2026-09-29 21:37:09 +00:00
jschoubben 9dc49cd831 Merge pull request 'Grooming: five issues were fixed and never closed, and one is not' (#187) from grooming/stale-issues into main 2026-09-29 21:37:04 +00:00
jschoubben 66b413a076 Merge pull request 'Issue 140 is resolved, and was resolved before it was read again' (#186) from issue/140-resolved into main 2026-09-29 21:36:57 +00:00
jschoubben fcba05fed9 Merge pull request 'Issue 146: a first node now enrols, and is enrolled twice' (#185) from issue/146-diagnosis into main 2026-09-29 21:36:42 +00:00
jschoubben 18f37c25b2 Issue 152: the loop is metastable, and it cleared at 23:11
It ran 22:46-23:11, five full replacements, and stopped when a pass
happened to read the store during a window it was up. The record said
nothing about it stops; that was wrong. Exiting by luck is the finding,
not a mitigation.
2026-09-29 23:27:51 +02:00
jschoubben 3c2b4fc6b6 Issue 152 is fixed: a lookup failure is no longer an absence
The three gatherers pass over a node whose set does not compose, and
raise anything else. 151 stays open: this removes the false reasons a
roster changes, not the fact that a real change still replaces every
container.
2026-09-29 23:26:20 +02:00
jschoubben 72eaf52867 Issue 152: a node whose plan will not compose withdraws its names from every machine
ace numbered its two records 147 and 148, which this repository already
uses; they become 150 and 151, as 127 became 149.

The new record is why the control node could not stop applying: the
roster alternates between two values because routeNamesInTheMesh
swallows a per-node plan failure, and the roster is part of every
container's identity. The loop closes through the control plane's own
store, which each pass replaces.
2026-09-29 23:13:54 +02:00
jschoubben 741625e725 Merge remote-tracking branch 'origin/issue/147-148-found-migrating-ace' into issue/the-roster-flicker-recreates-every-container 2026-09-29 23:12:57 +02:00
jschoubben c3730b9a23 Issue 150: a machine's own network is not a reach
ADR 0138's reach is internal, public or both. ace's LAN devices (an IoT
switch on mosquitto, unifi access points, plex clients) are none of them;
the only reach that admits them is public, which states the wrong thing and
opens the port to the internet on any machine with a public address.
2026-09-29 23:06:08 +02:00
jschoubben 3a59099c81 Issue 149: an adopted machine's data cannot be placed where it is
ADR 0112 decided that an assignment places a directory and says where an
access's data is; the control plane implements neither. ace's media stack
(a 40 TB ZFS library, plex state on a second disk) cannot be expressed
without writing ace's paths into manifests.
2026-09-29 23:03:11 +02:00
jschoubben 3bd6f34de3 Issues 147 and 148: a route before its module is taken; a new name recreates every container
Both found migrating the first module on ace (searxng), adopted with route-adapter:
147 — assign is not a no-op for a routed module: its route reaches the predecessor's
proxy while its container is held, and the name serves a dead backend until take.
148 — every push that moved the mesh's names replaced every container on novox, twice,
the control plane's own store included.
2026-09-29 23:01:51 +02:00
jschoubben 2b5119ecd2 Issue 114 is answered: the controller is a process, by ADR 0142
It asked a question rather than reporting a defect, and the question was taken
five days later — the mesh's own components are binaries on the machine, and
third-party software stays a container because an image is the right way to
carry somebody else's build. The delivery of them is issue 142.
2026-09-29 22:38:11 +02:00
jschoubben 96bdffa9bc Two records were numbered 127; the second becomes 149, and is resolved
A number identifies a record, and two were given 127 on 2026-09-27. The one
three documents and three source files cite by number keeps it; the other
becomes 149, says so in its own heading, and its two inbound references are
repointed.

It is also resolved: an empty declaration is sent carrying owns_nothing rather
than skipped, and the host refuses an empty body that does not carry it, so
emptiness cannot be read as a truncated declaration.
2026-09-29 22:33:40 +02:00
jschoubben 4eb16f1028 Three proposed records were already built; two are still yours to call
0037 (where a module lives), 0113 (the vault makes every secret) and 0115 (one
assignment of a module per node) described arrangements the mesh has — a
catalogue of other people's software plus a table filled by module add, a vault
answering a secret requirement six modules make, and a rule the assignment
table's primary key already enforces. Each is accepted against what was built,
and says so in its own words.

0037's other half is not built: a manifest outside this catalogue has no check,
which is issue 148.

0068 (the lab takes requests) and 0114 (a shared credential rotates over two
credentials) stay proposed. Neither is built, and both are decisions rather
than records of something that happened.
2026-09-29 22:31:55 +02:00
jschoubben d199de40db Merge the 146/147 records, which 006's note links to 2026-09-29 22:31:46 +02:00
jschoubben 9a1dc4665c Grooming: issue 006's knowledge base is the predecessor's, and is gone
The record catches README claiming this repository is indexed into a knowledge
base. Nothing indexed it, and since the cut-over there is nothing to index it
into — the surface that answered is on the transport the mesh removed (issue
147). Noted where the record is, so the next reader does not go looking for a
search that cannot exist.
2026-09-29 22:26:13 +02:00
jschoubben 14be8576f8 Grooming: five issues were fixed and never closed, and one is not
088, 089, 120, 128 and 130 each name a commit that is on main and cites them —
the forge's address following a moved port, a route naming its endpoint, a
provisioner asking the backend what is there, the hosts file written into a
marked block, and undeclaring giving a unit back the state it was found in.
Each says it was closed by reading commits rather than by a run, so nobody
reads a green that was never measured.

129 stays located on purpose: ca-trust is merged and no machine holds it, so
the symptom it opened on is still true everywhere.
2026-09-29 22:25:25 +02:00
jschoubben ec8676c225 Issue 140 is resolved, and was resolved before it was read again
Written 2026-09-28, answered the same week by mesh-controller bdf965d and
c68d3a7, and never closed — so the mesh's own account said reach was declared
nowhere while three readers were reading it: the filter, the proxy's names, and
each authority's host policy. The resolution names them and what checks each.

What is left is retiring the older per-port keys the block replaces, which is
not a gap in what reach can say.
2026-09-29 22:19:24 +02:00
jschoubben 3c0f7082e6 Issue 147: the tool surface is not the mesh's, it is the predecessor's
Diagnosed from the configuration: the tool server is HAL's brain, a local
process on the workstation with the predecessor's broker URL in the assistant's
own config. No manifest, no assignment, no seat, no account. Nothing regressed
— the mesh removed a transport this program still dials, and the program was
never part of the mesh. The mesh has a tool model and nothing publishes an
operator-facing surface onto it.
2026-09-29 21:50:50 +02:00
jschoubben 0dd00e88b6 Issue 147: the operator's tools still dial the bus that was removed
Every tool call fails with 'AMQP not connected', on every node including the
local one, because the tool surface still opens an AMQP connection and that
transport was deleted at the cut-over. The mesh reports healthy throughout —
what broke is the thing standing outside asking it questions, so nothing the
mesh checks is about it.
2026-09-29 21:39:55 +02:00
jschoubben eef54917ec Issue 146: the double enrolment was two consumers sharing a delivery subject
Not about enrolment. A push consumer delivers onto an ordinary subject and
everything subscribed to it gets a copy; the controller's two consumers were
both named after it, so both were given the same subject and the one process
acted on every message twice. Enrolment is where it drew blood because a second
enrolment mints a second credential.
2026-09-29 21:32:52 +02:00
jschoubben e9b1010bc0 Issue 146: what made it slow, and what was changed so it is not 2026-09-29 17:45:25 +02:00
jschoubben f6ed3545b7 Issue 146: a first node now enrols, and is enrolled twice
Two more faults behind the three already fixed. The account a token is the
password of was never recorded, and the comment above the issuing code said
it was; issuing now records it. Placing the composed list at genesis is the
other half — the control plane says what it composed and whoever raises the
machine writes it beside the bus, because no declaration can reach a machine
that has not enrolled.

With that a first node enrols. It is then enrolled twice from one attempt,
each minting a credential, and it keeps the answer to the first while the mesh
keeps the second. The trail for that one stops at a duplicate that survived
message-id deduplication.
2026-09-29 17:36:40 +02:00
jschoubben 0b08cdfce1 Merge pull request 'ADR 0147: a module anchors the mesh's authority, and issue 146: the foundation cannot be raised' (#184) from decision/0147-a-module-anchors-the-meshs-authority into main 2026-09-29 14:06:41 +00:00
jschoubben bffd2af40c Issue 146 diagnosed: four faults stacked, three fixed, the fourth is genesis
Raising a first node hits them in order: the bundle's bus image named for a
registry that is gone; the certificate made by openssl in an image that has
none; enrolment dialling TLS at a bus that speaks first, then refusing its own
token for the empty bus. Each was right until the bus changed and nothing has
raised a foundation since.

The fourth is not a patch: the installer carries the bus's first user list and
the controller composes the rest through the bus being a module, and at genesis
there is no module — so the first node cannot be let onto the bus it just
raised.
2026-09-29 15:42:32 +02:00
jschoubben 4a51ea4b3a Issue 146: the foundation cannot be raised on the bus the mesh runs on
Found trying to run ADR 0147's bed. The older bundle raises the previous
broker and a control plane that refuses to start without MESH_BUS_NATS; the
newer one stops a step earlier, asking for a certificate from openssl in an
image that has none. No bed can run while this holds, so 0147's check section
now says what actually stands behind it — the rendering, not a machine.
2026-09-29 15:25:58 +02:00
jschoubben 6c14d313b8 ADR 0147: a module anchors the mesh's authority on a machine, and takes it away again
Issue 129: every internal HTTPS name fails verification on every machine,
because nothing has ever written the mesh's root into a trust store. The
report proposed the controller inject it the way the private network writes
the registry's trust; this record rejects that — reachability and trust are
not the same fact, and where anchors live is the host's difference, not the
controller's. A module requiring internal-acme-ca does the whole of it, and
being unassigned undoes it.
2026-09-29 15:02:37 +02:00
mesh-admin ced547dae9 Merge pull request 'ADR 0146: connectivity is checked by name, per hosting form' (#183) from decision/0146-connectivity-by-name into main 2026-09-29 12:01:57 +00:00
jschoubben 38482435af ADR 0146: connectivity is checked by name, per hosting form, with a valid certificate
0145's core stands — a module checks what the mesh claims, from where the callers
are — and everything it said about what to dial was wrong.

A raw port is not how anything in this mesh is reached. A real caller resolves a
name, the proxy answers, the proxy reaches the service; dialling a port tests the
last hop of a four-hop path and skips the three where most connectivity lives.

So each hosting form gets its own endpoint, route and name:
connect-docker.<node>.internal for a container, connect-process for the mesh's own
code in a unit it writes, connect-unit for a unit a package ships — and the same
under each public domain. Every machine checks every machine, by name, over TLS,
verifying the certificate against the authority that should have issued it. The
names are the instrument: a failure reads as connect-docker.g14.internal did not
answer.

No name is written anywhere: the machines come from the roster fact, the labels are
the module's, and a machine that joins is checked by the others on the next push.

Five things this needs that the mesh does not have, named rather than assumed —
a module every machine has (ScopeNode is exclusivity, not obligation), a container
running a bundle, a unit a package ships, the public domain in the roster fact, and
issue 129, until which every internal name will correctly fail verification.
2026-09-29 14:01:55 +02:00
mesh-admin cf8a8d78c9 Merge pull request 'ADR 0145: a module checks what the mesh claims is reachable' (#182) from decision/0145-a-module-checks-what-the-mesh-claims into main 2026-09-29 11:44:58 +00:00
jschoubben 9de25994e9 ADR 0145: a module checks what the mesh claims is reachable, and it checks itself
The mesh asserts three things are callable and has never checked any of them. A
module on every machine serves an endpoint of its own and dials every other
machine's, from the position the callers are in — not the host and not the control
plane, both of which reach those addresses by paths no ordinary caller uses and
would have passed throughout the outage.

Its probe is its own endpoint, declared reachable over the private network, so it
is admitted by exactly the rule that governs every internally-exposed service. The
tempting target is a service every machine has, and those are the ones never closed
— ssh above all — which would have passed for all eleven hours.

Resolution and connection reported separately, because they have different owners.
One failure is not a fault, and the count travels with the result. It reports and
repairs nothing.

Built on a scheduled container, a rendered roster fact and a bundle: no new
vocabulary. Adopted on its own merits, which is what 0143 was not.
2026-09-29 13:44:56 +02:00
mesh-admin 64ea47b11d Merge pull request 'ADR 0144: anything on a machine may call anything on it' (#181) from decision/0144-local-is-not-a-boundary into main 2026-09-29 11:32:03 +00:00
jschoubben eba24a72af ADR 0144: anything on a machine may call anything on it, superseding 0143
Everything should be able to call what runs on the same machine, another machine's
service exposed to the private network, and another machine's service exposed
publicly. Three cases; the filter had two.

The first was expressed as the machines' own addresses on the private network. A
caller on the machine carries such an address; a caller in one of its containers
carries a bridge address and matched nothing — measured, same destination and same
machine: src 10.10.0.1 against src 172.17.0.8. The second case worked by accident,
because the tunnel rewrites a caller's address to the sending machine's. Two of
three working is why it read as correct.

0143 answered the wrong question. It proposed verifying each grant from the
consumer's own network position and went to length about which position, because
whether a caller sat in a container changed the answer — and that difference was the
bug. Observing a configuration error is not its remedy. Superseded, and nothing
replaces it; whether the mesh should check a grant is still open in issue 145 and
must stand on its own.

And a module is not a container: 61 of 72 happen to use one, 11 do not, and a rule
reasoning about containers describes most of the mesh rather than the mesh.
2026-09-29 13:32:01 +02:00
mesh-admin 78d4873f4f Merge pull request 'ADR 0143: a consumer verifies the grant it is given' (#180) from decision/0143-a-consumer-verifies-its-grant into main 2026-09-29 11:07:20 +00:00
jschoubben ddfd62edf6 ADR 0143: a consumer verifies the grant it is given
A grant is four facts and a credential — the provision, the machine, the port, and
who the consumer is when it connects — and it is the whole mechanism by which
anything in the mesh reaches anything else. The mesh asserted it and never found
out whether it was true. Issue 145 is what that cost: eleven hours of 'every module
current with its source' while a module could not reach its database.

The consumer verifies it, from its own network position. Not the control plane,
which reaches the address by a path no consumer uses. Not the machine, whose own
packets carried a source address the filter admitted while every container's was
refused — so a host-side check would have passed throughout the outage it exists to
catch. That is inference from the rule that was loaded, stated as such.

A connection and nothing more; speaking each provision's protocol would be a second
implementation of every provision. One failure is not news, only consecutive ones,
and the count is reported rather than the last attempt. A consumer that is not
running reads unchecked, which is a different sentence from broken.

What it costs to be wrong is the constraint on all of it: a check that calls a
working provision broken trains a reader to ignore the report.
2026-09-29 13:07:18 +02:00
mesh-admin f65664640a Merge pull request 'Issue 145: a machine reads healthy while its modules cannot reach each other' (#179) from issue/145-healthy-while-broken into main 2026-09-29 10:37:10 +00:00
jschoubben 492ac7be18 Issue 145: a machine reads healthy while its modules cannot reach each other
Converging the control node closed every path by which a module reached another by
the machine's own name, and it ran for eleven hours while the mesh answered 'all
doing what they were told, all heard from, running what the mesh would send them,
and every module current with its source'.

6,154 database failures in one module's log, beginning at the minute of the flip.
The service accepted TCP and never answered HTTP. Every check the mesh makes
passed, because every check the mesh makes is about the relationship between the
mesh and a machine — applied, current, containers running. None asks whether a
module can reach what it requires, though the mesh composes every grant and so
knows exactly who requires what.

The filter fault is fixed. The eleven hours are the measurement, not the bug.

Also records on 144 that 'more closed than the mesh believes' was harmless only
for what is reached from outside, and an outage for what is reached from within.
2026-09-29 12:37:08 +02:00
mesh-admin 5ac77e3cef Merge pull request 'ADR 0138: reach asks for names on a routed endpoint' (#178) from decision/0138-insight-reach-and-the-proxy into main 2026-09-29 00:50:28 +00:00
jschoubben a619022c35 ADR 0138: a progressive insight — reach asks for names on a routed endpoint
The record says internal and public each mean something to the filter. For an
endpoint the proxy serves, the second half is wrong, and ADR 0045 said so first:
a public service is exposed through the proxy, listening from the mesh, not by
opening its own port.

Found by trying to express one real module, not by review — routed name public
because browsers post to it, machine port private because it serves a dashboard in
cleartext. Under one value for both, saying public would have reopened a port an
operator had just closed. Measured the same evening: the routed name answered from
the internet over TLS while the port was refused from the same place.

Corrects a fact. One statement per endpoint with three things derived from it
stands; the filter column applies to an unrouted endpoint.
2026-09-29 02:50:25 +02:00
mesh-admin 49c065c204 Merge pull request 'Issue 143: correct the diagnosis' (#177) from issue/143-corrected-diagnosis into main 2026-09-29 00:33:13 +00:00
jschoubben ddb3980f09 Issue 143: correct the diagnosis — the step exists and did not fire
The first account said the declaration carries no resource that would disable the
found firewall and that nothing implemented the sentence the flip prints. Wrong:
the mechanism is a step in the host's own apply, retireFirewall, and it is careful
— it reads back that the mesh's table is loaded before retiring anything, records
the forward policies first, and verifies ufw reports inactive afterwards.

What is established: ufw was active and enabled two minutes after a flip that
reported the node converged with nothing failed; and the machine's record now
reads disabled_by_mesh: true, written by a reconcile fifty minutes later that
found ufw already inactive because an operator had disabled it by hand.

So the step did not take effect and the record says it did. The candidates are
named rather than chosen — the step is skipped silently when the apply has any
failure, the mesh's table is loaded by a service in the same apply so ordering is
open, and the host's detail lines do not reach the journal, which is why this has
candidates instead of a cause.
2026-09-29 02:33:10 +02:00
mesh-admin 75a8f6abc7 Merge pull request 'Issues 143 and 144: the found firewall is neither retired nor all of it' (#176) from issue/143-and-144-the-found-firewall into main 2026-09-28 23:36:10 +00:00
jschoubben 862253518f Issues 143 and 144: the found firewall is neither retired nor all of it
Both found by converging the control node — the first machine with a firewall to
flip, since the two before it had none.

143: the preview and the flip both say the found firewall is disabled. The node
reported converged, 372 resources applied, nothing failed, and ufw is still
enabled and active. A converged node's declaration carries no resource that would
disable it; the sentence is printed by the command and nothing implements it.

144: ufw was never what filtered the traffic that mattered there. Fifty forwarded
openings converged through it had matched zero packets, while a chain the
predecessor installed in the container runtime's pre-accept hook did the work —
in memory only, recreated by nothing. The mesh's filter now covers that path, so
the machine no longer depends on it, but the chain remains and is the only thing
refusing the bus and the registry, which the design requires reachable from
anywhere so a machine can enrol before it has a private address.
2026-09-29 01:36:08 +02:00
mesh-admin 51ef3eb7e2 Merge pull request 'ADR 0142: the mesh delivers its own components as binaries' (#175) from decision/0142-mesh-delivers-its-own-components into main 2026-09-28 22:51:44 +00:00
jschoubben 346e613995 ADR 0142: the mesh delivers its own components as binaries, not container images
Measured: the host is a binary somebody copied to four machines, owned by no
package and built by nothing, while the controller, catalogue, builder and vault
are container images publishing no ports at all. Same language, same project,
same kind of work, delivered two ways — and the difference is not a judgement
about either, it is that images are the only delivery that works.

What it costs: genesis must raise a container runtime before the control plane
can exist; updating the control plane goes through a registry the control plane
runs; a host change cannot be rolled out at all; and compiling the language the
mesh is written in is not a capability of the builder, so the controller is built
from a hand-written Dockerfile — the incantation the bundle toolchain exists to
abolish.

Third-party software stays a container: the store, the registry, the broker are
somebody else's build. The container runtime stays on the machine for modules.
What changes is that the control plane no longer needs it to exist.

The receiving half is already built and tested (ADR 0141). Staged: compile Go, an
artifact names its target, deliver a binary, the host first, then the rest, genesis
last.
2026-09-29 00:51:42 +02:00
mesh-admin 25ca9898d5 Merge pull request 'ADR 0141: a progressive insight on what delivery costs' (#174) from decision/0141-progressive-insight-on-delivery into main 2026-09-28 22:32:17 +00:00
jschoubben 709c095387 ADR 0141: a progressive insight — the delivery is not 'nothing new'
The record claimed a version reaches a machine as an ordinary archive with
nothing new needed. Two things it needs do not exist: no toolchain can compile
the host (the list is typescript and python, and the control plane, also Go, is
built as an image from a Dockerfile instead), and nothing interpolates a built
version into a resource path, so nothing can ask for .../versions/<version>/.

The decision, the options weighed and every consequence stand — the host half is
merged and tested. What was understated was the cost, so it is corrected in place
and dated rather than superseded.
2026-09-29 00:32:14 +02:00
mesh-admin c262de3833 Merge pull request 'ADR 0141: the host delivers its own successor' (#173) from decision/0141-the-host-delivers-its-own-successor into main 2026-09-28 22:22:28 +00:00
jschoubben a815433214 ADR 0141: the host delivers its own successor, and versions live side by side
The supervision was already right — a clean exit means the host stood aside and
the launcher runs what is on disk, failures are counted, and a rollback happens
at the limit. Two things made it dead code: nothing told the running host a
successor was waiting, and the rollback resolved its known-good version through
pacman, which no machine here uses and which two of three operating systems do
not have.

Keeping a version rather than a path was the clue. Versions live side by side in
directories named for them; the newest runs; the running one stands aside between
reconciles; a reconcile that completes records itself and retires what is older
than its predecessor; rollback starts that predecessor. No new resource kind and
nothing new on the bus — an archive already fetches by digest, and the path
written is never the path executing.

Answers issue 142.
2026-09-29 00:22:04 +02:00
mesh-admin 61dc90e508 Merge pull request 'Issue 142: the host is the one thing the mesh does not deliver' (#172) from issue/142-the-host-is-not-delivered into main 2026-09-28 22:03:58 +00:00
jschoubben a7cf5c0c1b Issue 142: the host is the one thing the mesh does not deliver
A host change merged yesterday reached no machine without a person copying a
file. The host is not a build target, no declaration delivers it, and the half
that recovers from a bad host — noticing the executable changed, a known-good
record, a launcher that rolls back — is written, tested and called by nothing.
All four machines run a byte-identical hand-copied binary that no package owns
and no record names, so nothing can say a machine is behind.

Found because ADR 0140 needs the machine to report a new fact, and merging that
could not roll it out.
2026-09-29 00:03:41 +02:00
mesh-admin c6f86ae935 Merge pull request 'ADR 0140: the filter constrains what arrives from outside' (#171) from decision/0140-filter-constrains-what-arrives-from-outside into main 2026-09-28 21:32:10 +00:00
jschoubben 1aeb4fe8d8 ADR 0140: the filter constrains what arrives from outside, and says nothing about a machine's own guests
Reading a converged machine's rendered rules showed the cause: the chain blocks
everything passing through and then allows the machine's own containers back by
listing their address ranges. 0137 made that list typeable and 0139 tried to
generate it; both refined a list that should not exist, because the mesh has no
position on a container reaching outward. Constrain what arrives from outside,
allow what did not, and let the machine report which links face outside — one
fact instead of a list. Ports keep following the modules unchanged.

The records check now allows one record to supersede several, and stops
requiring a withdrawn record's own citations to be live.
2026-09-28 23:31:50 +02:00
mesh-admin 08108b569d Merge pull request 'ADRs 0138 and 0139: an endpoint's reach, and networks forwarded because a module declared them' (#170) from decision/0138-endpoint-reach-and-0139-declared-networks into main 2026-09-28 21:03:49 +00:00
jschoubben 14ff89fa40 ADRs 0138 and 0139: an assignment binds an endpoint and says how far it reaches, and a network is forwarded because a module declared it
Both follow from the same rule the mesh is built on — a node's configuration is
composed from the modules assigned to it. Reach was settled separately by the
filter, the proxy's names and the certificate authority, so "this must not be
public" could not be written; it becomes one value on the assignment that all
three read. And the forward chain consulted two constants plus a typed list
although modules already declare their networks; it now forwards what they
declared, with the host rendering the addresses it allocated.
2026-09-28 23:03:32 +02:00
mesh-admin 2126e7b2cb Merge pull request 'Issues 140 and 141: an endpoint's reach, and a forward chain that does not follow the modules' (#169) from issue/140-endpoint-reach-and-141-forward-chain into main 2026-09-28 20:58:03 +00:00
jschoubben dcdfcf104e Issues 140 and 141: an endpoint's reach is declared nowhere, and the forward chain follows constants instead of the modules
Found preparing the control-node's convergence. Reach is settled independently by
the filter, the proxy's names and the certificate authority, so "this must not be
public" cannot be written and a public certificate is obtained regardless. And the
forward chain allows two hardcoded ranges plus a typed list, though the mesh
already knows which networks exist because its own modules declared them — a range
wide enough to keep four of them would have forwarded two predecessor leftovers too.
2026-09-28 22:57:38 +02:00
mesh-admin d23ace1646 Merge pull request 'Issues 138 and 139' (#168) from issue/138-the-uplink-seat-and-139-an-internal-route-name into main 2026-09-28 19:46:22 +00:00
jschoubben 5dbde0b13a Issues 138 and 139: a seat with interchangeable holders that are not, and an internal route name that resolves to the wrong machine 2026-09-28 21:46:20 +02:00
mesh-admin db3868e2b0 Merge pull request 'ADR 0137: a machine says which networks it routes' (#167) from decision/0137-a-machine-says-which-networks-it-routes into main 2026-09-28 19:31:03 +00:00
jschoubben 05039c4f10 ADR 0137: a machine says which networks it routes, and issue 137 is how that was found 2026-09-28 21:31:01 +02:00
mesh-admin b7bf601ca4 Merge pull request 'Issue 136: a module may name a program the machine does not have' (#166) from issue/136-a-module-may-name-a-program-the-machine-lacks into main 2026-09-28 18:50:37 +00:00
jschoubben bff32e3370 Issue 136: a module may name a program the machine does not have, and everything reports success 2026-09-28 20:50:35 +02:00
mesh-admin d6b62387f2 Merge pull request 'Issue 135: a container's mesh names are not compared' (#165) from issue/135-a-containers-mesh-names into main 2026-09-28 15:19:37 +00:00
jschoubben 4789624857 Issue 135: a container's mesh names are not compared, so a moved address is never noticed
One container restarted 2286 times over five days while the mesh reported the machine as doing what it
was told. Its overlay address was five days out of date: the host compares a container by a digest of
its spec, and the mesh's names were not in it, so a container whose image and files never changed was
left alone holding a name that no longer resolved. Forty-eight others were current only because
something else had recreated them.

The same fault as issue 045, in the field that was left out. Resolved by putting the names in the
digest.
2026-09-28 17:19:35 +02:00
mesh-admin e00862e317 Merge pull request 'ADR 0112 is accepted, and issue 134 records what it is not yet' (#164) from decision/0112-accepted-and-issue-134 into main 2026-09-28 15:12:15 +00:00
jschoubben 3a92e4b80c ADR 0112 is accepted, and issue 134 records what it is not yet
0120 was already accepted; what failed the check was that it rests on 0112, still marked proposed —
and so do four designs. The decision stands: a definition names no node, no mesh and no host path, and
everything a module needs is a requirement the mesh resolves.

Accepting it makes the gap visible rather than hiding it, so issue 134 states it. 0112 says how it is
checked — 'a catalogue test finds no domain name in any definition value' — and there is no such test.
Asked by hand: seven modules name this installation in a value the mesh acts on, and eight mention a
public name in prose nothing reads. The two are not the same fault and the fixes differ, which is why
the issue separates them rather than counting to fifteen.
2026-09-28 17:12:12 +02:00
mesh-admin bfddf78bf3 Merge pull request 'Design 32: what shipped today, and the records it rests on' (#163) from design/28-and-32-what-shipped into main 2026-09-28 14:50:44 +00:00
jschoubben 0b291d89f3 Design 32: what shipped today, and the records it rests on
Two of its statements are built — a version preparing its state, and the mesh saying what it applied —
so the document is in-progress rather than proposed, and names the code that owns them. Resting on a
live record rather than a superseded one: 0127 was replaced by 0131.

What this exposes is pre-existing: it also rests on ADR 0112, which is still proposed, and a document
that is not itself proposed may not. ADR 0120 has rested on it the same way for a while. Accepting or
superseding 0112 is a decision, not a cleanup, so it stays visible in the check rather than papered
over.
2026-09-28 16:50:41 +02:00
mesh-admin a924efcc28 Merge pull request 'ADR 0136: a step gates its module, not the machine' (#162) from decision/0136-a-step-gates-its-module into main 2026-09-28 13:38:18 +00:00
jschoubben b421c73a5a ADR 0136: a step gates its module, not the machine
ADR 0135 made a step something the mesh derives for any module that prepares its state, which turned
ADR 0052's reach into a fault: a module whose database is briefly unreachable would stop every module
declared after it on that machine — the fault issue 011 already removed for every other shape, and
the reason the catalogue migrates itself at start rather than in a step.

A step now stops the rest of its own module and nothing else; an action still gates the machine,
because genesis is a row of them and they belong to no module. What was not attempted is reported as
skipped rather than left to be inferred from silence.
2026-09-28 15:38:16 +02:00
mesh-admin 017b1401e4 Merge pull request 'ADR 0135: a module version prepares its state before it runs' (#161) from decision/0133-0134-migrations-and-deploy-facts into main 2026-09-28 09:57:47 +00:00
jschoubben 476cda417d ADR 0135 supersedes 0133: a module version prepares its state before it runs
Two faults in 0133, both caught on review. It put the declaration on a container — one resource kind
the host applies — so every author would restate the machine's arrangement and a module's own
lifecycle would be tied to how its artifact happens to run. A module declares entrypoints for its
tools and its provisioner; preparing its state is the same vocabulary and nothing about a runtime.

And it derived the scope from the machine, which the facts already answer: a consumer is a module on
a machine (issue 022, migration 0015), so what the mesh provisions is per consumer. A module on three
machines has three databases, there is no shared state to race over, and the lock obligation 0133
invented was for a situation the mesh does not produce. The level question HAL answered with stages
dissolves — the scope of preparation is the scope of the state, and the mesh knows it.

0133 keeps its reasoning and gains a pointer; design 32 and issue 133 name the live record.
2026-09-28 11:57:16 +02:00
mesh-admin 74ba3ff1d4 Merge pull request 'ADR 0133 and 0134: who runs migrations, and the mesh saying what it applied' (#160) from decision/0133-0134-migrations-and-deploy-facts into main 2026-09-28 09:45:49 +00:00
jschoubben 891c7a945e ADR 0133 and 0134: who runs migrations, and the mesh saying what it applied
0133 — a module owns its migrations and the mesh owns when they run. A container declares what must
run before it; the mesh derives the gated step from the resource it precedes, so the image, the
environment and the credentials come from the one place they are described. The module owns the SQL,
the dialect and the lock; the mesh owns the moment and refuses to start a version whose step failed.
Per node, with no level: a step that ran once somewhere leaves every other machine ungated, and
'once, mesh-wide' is what holding a seat already means.

0134 — the pipeline is observable from a merge to an artifact and goes dark at the machine. What a
node now runs, and what it refused, become facts under the control plane's own seat, emitted when
what a machine runs changes rather than on every convergence pass.

Design 32's lifecycle carries both; issue 133 points at them as what ends the matter it opened.
2026-09-28 11:45:47 +02:00
mesh-admin 83cbeb8db8 Merge pull request 'Issue 133: the control plane's schema is migrated at birth and never again' (#159) from issue/133-the-control-plane-migrates-before-it-serves into main 2026-09-28 08:27:32 +00:00
jschoubben ab6db9369b Issue 133: the control plane's schema is migrated at birth and never again
The mesh replaced its own control plane with a build carrying a migration, applied none of it, and
then recorded no build for three quarters of an hour while saying everything was fine. ADR 0052
already prescribes the shape — a run-once step that gates the server — and the control plane was the
one module that did not use it.
2026-09-28 10:27:30 +02:00
mesh-admin 84571b4825 Merge pull request 'ADR 0132: a seat carries the tools its holder must serve' (#158) from decision/0132-a-seat-carries-the-tools-its-holder-must-serve into main 2026-09-28 08:17:01 +00:00
jschoubben d57196102d ADR 0132: a seat carries the tools its holder must serve
A role's tools belong to the role, not to whichever module holds it today: the seat declares them
with their schemas, serving them is a condition of occupying the seat, and what the mesh can do
becomes a read of its own records rather than a question nothing answers. A module keeps its own
tools — the same module may run without the seat, and then only its own name is true.

Design 33 follows: the three families, addressing a node-scoped seat, discovery, and what serves
this to an agent.
2026-09-28 10:16:59 +02:00
mesh-admin e6402cf777 Merge pull request 'Issue 132: a module can be recorded without the directory it lives in' (#157) from issue/132-a-module-can-be-recorded-without-its-directory into main 2026-09-28 07:20:07 +00:00
jschoubben 4f0d144833 Issue 132: a module can be recorded without the directory it lives in
Nine modules could not be rebuilt: their record named the repository and no directory, so every
build looked for a manifest at a repository root that has never had one. Resolved by mesh-controller
— `module add` takes the directory and the forge, and the rule is checked rather than described.
2026-09-28 09:20:05 +02:00
mesh-admin 98d94ef71e Merge pull request 'Design 28: 5.5 done, the mesh has one bus; issue 131 resolved' (#156) from design/28-one-bus-issue-131-resolved into main 2026-09-28 01:59:41 +00:00
jschoubben 4e13280604 Design 28: 5.5 done, the mesh has one bus; issue 131 resolved
The AMQP transport is gone from the control plane and the hosts (mesh-controller #112,
mesh-host #39). On the way: no build had ever recorded its bases, so every bases-first order
walked an empty graph; the builder now reports what it was handed and the graph is read from
builds (mesh-controller #113/#114). Issue 131 is resolved by the forge module's merge event,
the control plane following it, and those edges.
2026-09-28 03:59:39 +02:00
mesh-admin 8783a13448 Merge pull request 'Design 28: the mesh runs on the new bus' (#155) from design/28-the-mesh-runs-on-nats into main 2026-09-28 00:40:29 +00:00
jschoubben a31cfcf461 Design 28: 5.3 is built and was used for the hand-over 2026-09-28 02:28:06 +02:00
jschoubben 75b3861911 Design 28: the mesh runs on the new bus
Tasks 4.3, 5.2 and 5.4 are done as of 2026-09-28 02:25: every machine reports on
the new bus, the seat is held by the module that provides it, the old broker is
unassigned and forgotten, and every credential was minted afresh at the end.

5.2 records what it took, in the order it was found and each fixed on the trunk
before the next step, and how the bootstrap loop was broken once, by hand.
2026-09-28 02:27:49 +02:00
jschoubben 694555214a Merge pull request 'Design 26: which assignment holds a seat is on record, and changes as one act' (#153) from design/26-a-seat-is-held-on-record into main 2026-09-27 21:22:56 +00:00
jschoubben a7249541df Design 26: which assignment holds a seat is on record, and changes as one act
Until now the holder was derived — assigned and claiming — and a second eligible
assignment was refused, so a seat could not pass from one holder to the next
without a moment where nobody held it. The controller finds its own bus through
one of these seats, and that moment took the control plane down on 2026-09-27.

The holder is now a row the controller keeps, written by `seat <name> --to
<node>/<module>` in the same write that removes the previous one. No row means the
old rule, so nothing changes for a mesh that never hands a seat over; with a row,
another eligible assignment is silent rather than refused, which is what lets the
next holder run beside the current one until the switch. A holding is the
assignment's and goes when it does. Each rule names the test that checks it.

Under ADR 0131; design 28 task 5.3 is the work.
2026-09-27 23:20:36 +02:00
jschoubben 95d8253f71 Merge pull request 'ADR 0131: everything on the mesh speaks to the broker seat, and AMQP is not a provision' (#152) from decision/0131-everything-speaks-to-the-broker-seat into main 2026-09-27 21:06:02 +00:00
jschoubben 784b487bf9 ADR 0131: everything on the mesh speaks to the broker seat, and AMQP is not a provision
Taken during the outage of 2026-09-27, when the protocol leaked into the seat's
contract: to hold mesh-broker a module had to provide amqp, so the module that
will carry the bus could not hold the seat that names the bus, while the module
being retired could. Supersedes 0127. Modules depend on the seat and reach the
bus through the sdk; no manifest provides or requires amqp; the old broker's
module and the two modules that required it leave the catalogue; the AMQP
transport is deleted once every node reports on the new bus.

Design 28 step 5 rewritten under it: the seat handover becomes its own task and
is built first, because the seat the control plane dereferences cannot be empty
in between — that emptiness was the outage. The cost note now carries what was
measured rather than what was assumed.

0128 and 0130 extended 0127; each now rests on 0131 with a dated note and
changes nothing it decided. Every other citation of 0127 names its replacement.
records.py still fails on 0120/0112, which predates this branch.
2026-09-27 23:03:05 +02:00
jschoubben 7ae711ba0b Merge pull request 'Building the bus: the decisions the work needed, and what it taught back' (#150) from feat/nats-genesis into main 2026-09-27 17:06:40 +00:00
jschoubben 51739c4302 The order the repositories land in is part of the rollout
Derived while merging and visible from no single repository, so it belongs
written down rather than re-derived later: the client library before the
catalogue, because it is where the subject is derived and a converted module
against the old one publishes the local name itself; the catalogue before the
controller, because the controller refuses an old-style name outright and would
make every unconverted module unregisterable.

Two tested properties are what make it safe and not merely ordered. An old-style
name passes through the derivation untouched, so an unconverted module keeps
working at every step. And a converted name derives to exactly the key the old bus
published, so nothing moves on the wire until 5.2 sets the variable.

The failure avoided is 127's own, which is why this is worth a table: a publisher
and a subscriber disagreeing about a subject log nothing anywhere.
2026-09-27 19:04:45 +02:00
jschoubben 5cac268457 Two checks were wrong about where a seat is judged
Correcting design 26 to match what the merged code does, and fixing issue 112's
status, which used a word the vocabulary does not have.

A claim on a seat the manifest does not itself declare is refused at registration
rather than by the parser. A module may hold a seat another module declared —
that is why ADR 0126 has a caller name the seat and not its provider — so whether
the name exists is a fact about the whole catalogue.

And a declared seat may promise nothing. That is a marker seat, and most
node-scoped seats are markers: which module is this machine's packet filter. ADR
0126's "a declared seat carries a protocol" says what a holder must satisfy, not
that every seat offers something.

`records.py` still fails on ADR 0120 resting on a proposed ADR 0112, which is not
this branch's and not mine to decide.
2026-09-27 18:57:49 +02:00
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
2026-09-27 18:23:41 +02:00
jschoubben 9ef9830dcf ADR 0122: the predecessor is ending, and its broker goes with it
ADR 0119 rejected giving the old broker a retirement condition and said why: "its
clients are not only the predecessor's, so the retirement condition describes a day
that will not come". The operator has said that day is coming — the predecessor is
deprecated, some of it still running, none of it being migrated, left to stop rather
than moved.

Recorded because three documents reason from the premise it overturns. Design 25 §5's
"no day anything is waiting for", §9's "the predecessor's clients never notice", and
design 28's closing note that the predecessor's world does not need to move.

**And it needs no new machinery, which is 0119 being paid off rather than revised.**
Because that record made the broker an ordinary provider rather than a compatibility
module, ending it is unassigning a provider whose provision nothing requires — something
the module system has done since it existed. So step 5.3 finishes instead of trailing
off, and the transitional double announcement of a build outcome has a date.

The consequence worth planning around: the predecessor's own mesh talks over that
broker, so shutting it down ends the tooling that reaches this installation's machines
from a workstation. The rollout is driven from the node, or driven before the broker
stops. That is a sequencing constraint on 5.2, not an afterthought.

What survives is `amqp` as a provision: a module that genuinely needs an AMQP broker can
still be given one. What retires is this broker's role as the predecessor's.
2026-09-27 18:11:11 +02:00
jschoubben dfac01a6dd 5.2: the readiness half is in, and what a failed move actually costs
`rollout check` answers from records whether this mesh could move its bus, and names the
next step for each thing missing. The move itself waits on that check having been run
against a real mesh — writing the irreversible half before its question has ever been
asked of something real breaks the plan's own rule about beds by another route.

And the cost of being wrong is written down rather than assumed: the old broker stays for
its other clients, nothing in a served request's path goes over the mesh's own bus, and
what a failed move costs is the mesh's ability to change things rather than the services
its modules serve.
2026-09-27 17:59:26 +02:00
jschoubben 51f5ce5c0f 4.5 done: the catch-up needed nothing built, which was the answer
Three ways to do the replay were weighed and the right answer was that the bus being
moved to already does it. A queue on the old bus receives only what is published after
it is bound, so everything built before the catalogue existed was announced to nobody. A
stream is a log and a consumer is a position in it: a consumer created later starts at
the beginning and the builds are simply there. Checked against a running server, because
the decision rested on it.

So "who replays" has no answer because nothing replays. The mechanism was never about
builds — it was about a queue that could not remember, and carrying it across would have
carried a workaround for a limitation that no longer exists, with nothing looking wrong.

Retiring it belongs to step 5, with the rest of what only the old bus needs.
2026-09-27 17:23:49 +02:00
jschoubben fc64a2c1a4 4.4 done: a person's account and their client
The account existed as a permission model and as nothing a person could be given; there
is a record and three commands now. The client is two surfaces over one thing, a command
line and an MCP server, both using the client a module's runtime uses — so what a person
may do is answered by the same permission list that answers it for a module.

Design 25 §7 says nothing of this is built before its bed passes, and this was built
before. Noted in the task rather than quietly ignored.
2026-09-27 17:07:28 +02:00
jschoubben 9fc4e74b0d Merge pull request 'to-be 31: a module declares its fail2ban jail, mesh composes them per node' (#149) from design/a-module-declares-its-jail into main 2026-09-27 15:00:16 +00:00
jschoubben 225dfa9451 to-be 31: a module declares its fail2ban jail, mesh composes them per node
A node's intrusion filter should be composed from its assigned modules, like
its firewall (the Filtering mechanism): a service module (postgres, mssql,
mailu) declares its jail in its manifest (filter + stanza, no node/path per ADR
0112), and the mesh writes the jails of a node's modules into the fail2ban
holder's jail.d. The base (sshd, recidive, ignoreip=mesh-range) stays the
fail2ban module's. Records the model after novox's HAL per-module jails were
lost as dangling symlinks; the ignoreip is now safe on disk, the service jails
need this to be restored.
2026-09-27 16:59:56 +02:00
jschoubben bfefb1dbdb 4.3: the installer can raise a mesh on the new bus
A foundation template that stands the server up, writes its settings and the mesh's
first user list beside them, and starts a controller on the new bus. The first user list
is the installer's because at genesis there is no mesh to compose one — a bootstrap
credential, rotated like the store's.

The carried list is checked against what the controller derives, since a mesh cannot be
raised twice to discover they disagreed. That check immediately found the composer
granting a role's whole event branch as well as the one event it follows.

What is left of 4.3 is running it, which is 4.1's bed.
2026-09-27 16:39:48 +02:00
jschoubben 85c0a3e567 4.2 done: a build is work submitted to a role, on both buses
Both sides behind a seam, one implementation per bus, and the outcome is the role's
own event so one publish reaches the asker, the controller and the catalogue. Checked
against a running server, including the part the decision rests on: a third party
hears the same outcome the asker does.
2026-09-27 16:02:17 +02:00
jschoubben a39765c924 Merge pull request 'ADR 0122: a seat is data the controller owns; a rename is a database update' (#148) from design/a-seat-is-data into main 2026-09-27 13:41:10 +00:00
jschoubben a8921fe737 ADR 0122: a seat is data the controller owns; a rename is a database update
Reviews 0110/0121 after a session where renaming seats cost three freezes, a
builder deadlock, and hand-resolved manifests. The seat rules were right; the
set being a compiled Go slice referenced by name-string everywhere was the
mistake. Seats become a table keyed by a stable id; claims/held/production code
reference the id; a rename is one UPDATE, no rebuild, no re-registration, no
freeze. The build machine reads the set from the mesh instead of embedding it,
removing the controller/builder seat coupling. Closed set and scope naming
unchanged; only storage and reference change. Outstanding renames (registry
seats, private-network scope) wait for this — as data each is a write.
2026-09-27 15:40:55 +02:00
jschoubben c8f430290e ADR 0121: a seat carries the protocol of its role
The mesh's own seats said who does a job and nothing about what may be said to
them or by them, and that gap showed up three times in one day looking like three
different problems: a build machine with three audiences for one outcome and no way
to derive a grant for any of them; an event genuinely about a role with nowhere to
live but the namespace of whichever module holds that role today; and a catalogue
catching up on builds, where every option needed a grant the design refuses.

One cause — the mesh has roles it cannot describe. So the `mesh-*` seats take the
same three fields a module's seat has, and the machinery that already derives
authority, queues and consumers from a declared seat does it for these too.

Builds become work submitted to a role, and `mesh.build.request`,
`mesh.control.built` and the BUILDS stream retire. A work queue shared by several
build machines is exactly what a seat's `accepts` is, so a second mechanism for it
was two places a permission could be wrong. The outcome is the seat's own event,
which means one publish still reaches whoever asked, the controller that records it
and the catalogue that places it — the fan-out a shared exchange gave for free,
written as a subject the mesh derived rather than a topology somebody configured.

That also avoids the grant that ruled out the alternatives: no holder needs
permission to publish into an asker's inbox.

The blocking gap is now named rather than incidental: the shared library has no way
for a module to publish on a seat. The build machine is Go and reaches the bus
directly, so it is unaffected; the artifact-store event waits.
2026-09-27 15:24:17 +02:00
jschoubben d98d6fca11 Merge pull request 'to-be 30: the mesh updates itself on a push' (#147) from design/the-mesh-updates-itself into main 2026-09-27 12:49:32 +00:00
jschoubben b8cdfce16d to-be 30: the mesh updates itself on a push
Records the manual update process (module moved -> build -> reconcile; and the
breaking-change freeze/re-register recovery), and the two things that make
self-update more than a webhook: the build-on-push trigger is currently HAL's
(hal-gitea-tools on :9877), a retirement gap the mesh must replace with its own
forge-webhook trigger wired to every repo including mesh-controller; and the
builder validates manifests too, so a breaking change couples controller +
builder + manifests + hosts, and renaming the builder's own seat deadlocks its
rebuild. Names the transition discipline (accept old+new for one release) that
self-update needs so a push does not auto-freeze.
2026-09-27 14:49:02 +02:00
jschoubben c4a8455e2e Issue 127 resolved; design 29 says what wildcards are and how the rule is checked
Every module named its events the way the old bus spelled a routing key, so on the
new bus every cross-module subscription pointed at a namespace nobody publishes to.
Nothing failed — the services started and none of them reacted. Converted, and the
rule now has checks at both scales: at registration for one manifest, and as a test
across the whole catalogue where a consumed event's emitter is present.

It was larger than the report said, in two directions nobody had looked. Forty-three
files of module code pass the event name at runtime, so the code mattered as much as
the manifests. And both clients had to learn the mapping — without that, converting
the modules would have broken the mesh that is actually running, which is the
opposite of what fixing this was for.

Design 29 gained three things it did not say: what a wildcard is (`*` for one name,
`**` for the rest, spelled the mesh's way and derived to each bus's own), that an
event about a role belongs on the seat and why that is not yet possible, and how the
rule is checked — because "a subscription that matches nothing is silence" is exactly
why nobody noticed thirty-seven manifests being wrong the same way.

4.2 and 4.3 are unblocked. The catch-up half of 4.5 is not: it is a decision, and it
narrowed rather than closed. It cannot be a reply to a module's inbox, because that
needs the blanket grant design 25 §4 refuses.
2026-09-27 14:45:08 +02:00
jschoubben bf7297b7ed Merge pull request 'ADR 0121: keep distribution, retire only verdaccio; node-* seats migrated' (#146) from design/0121-keep-distribution-fix into main 2026-09-27 12:36:54 +00:00
jschoubben 1bb0ef5658 ADR 0121: keep distribution, retire only verdaccio; node-* seats migrated
Records the reversal: distribution stays as the mesh's OCI registry (it serves
every artifact-store:// image); only verdaccio, a redundant second npm registry,
is removed. The 'consolidate onto gitea / retire distribution' direction was
dropped. Also records that the node-* rename was executed as one controlled
migration with a brief compose freeze, and why the delivering registry seats
are deferred rather than folded in.
2026-09-27 14:36:33 +02:00
jschoubben 5a9917f802 Merge pull request 'ADR 0121: a system seat is named for its scope; a module may define its own' (#145) from design/system-seats-are-named-by-scope into main 2026-09-27 12:15:22 +00:00
jschoubben 8a6ee9177c ADR 0121: a system seat is named for its scope; a module may define its own
The control plane's seats grew a second naming style (the-*) beside mesh-*,
and the closed set was the only place any seat could be defined. This settles
both: system seats are mesh-* (one, mesh-wide) or node-* (one per node), named
for scope; a module may define its own seat outside the closed set. Folds in
the seat review: mesh-build-machine (scope fix), mesh-private-network (one
server + client modules, dropping per-node VPN choice), showcase becomes the
first module-defined seat, node-uplink, and the node-* renames — plus the
registry consolidation onto gitea, which reshapes the registry seats and gates
retiring distribution/verdaccio. Records why the renames are a coordinated
migration and why distribution cannot be removed until gitea serves images.
2026-09-27 14:15:05 +02:00
jschoubben dd577e9ebe 1.7 is done; 4.1 waits on nothing but the bed
Minting on both halves, the file delivered per push, and the bus's objects
asserted on every start — verified against a real server that asserting twice
changes nothing, that a machine joining an already-raised bus is accepted, that
each node's consumer is bound to its own declaration subject, and that CONTROL
does not dead-letter before the controller gives up.

Which bus the mesh is on is one fact, and being told about both is refused at
start rather than warned about: a mesh half on each is one where a declaration
goes out on one bus and the report comes back on the other while every component
logs success — ADR 0074's failure arriving through configuration rather than code.

So 4.1 no longer waits on code. Both links speak NATS, the composition happens,
and every claim behind them has a unit test or a check against a running server.
What none of those can stand in for is a mesh raising itself, which is what the bed
is — this is where the code stops and the lab starts.
2026-09-27 03:20:42 +02:00
jschoubben a9b8f41570 Design 25 §4: the server verifies no client certificate, and writes only accounts
Two corrections of fact, both found by building the module's image and connecting
to it as a host would.

The first composed configuration said `verify: true`, which makes the server
demand a *client* certificate — and nothing in the mesh presents one. A host pins
this server's exact certificate and authenticates with the password the mesh
minted, and so does a module's runtime. Every connection in the mesh would have
died at the TLS handshake before any password was looked at, with an error that
reads as a fault in the client. TLS is still required; verify only decides whether
client certificates are checked. Mutual TLS is a later question and would need
machinery the mesh does not have — a certificate per module per node.

And §4 read as though the controller wrote the whole file. It writes the user list
and nothing else: ports, TLS paths and a store directory belong to the container
the module raises. The two files share one directory of necessity, because an
absolute include path is resolved relative to the including file's own directory.

The decision stands in both cases — accounts are composed, not called for, and
passwords are minted and sealed. What changed is what the file says and who writes
which half.
2026-09-27 02:51:40 +02:00
jschoubben 92a5e8fc05 Merge pull request 'ADR 0120: a roster fact carries its format as a template; rewrite to-be 29' (#144) from design/roster-fact-is-a-template into main 2026-09-26 23:51:21 +00:00
jschoubben 8c91ba1cfa 1.7 in progress: the list is derived and the keys are kept
What is in: a bus user's hash is recorded and its plaintext returned once, and
the user list is derived from the machines, what each runs, every manifest and
which machines hold a live token. Permissions stay derived rather than stored,
because a stored copy could disagree with the records it came from while both
looked internally consistent.

What is out, with what each needs, so the next person does not rediscover it:
delivery, which has one open question about what a module declares in order to
receive the file — design 29's ground, not this document's; minting, which is
transport-coupled because an enrolment reply carries one password and a node on
the old bus must not be handed a credential for the new one; and calling the
assertions from a start path.
2026-09-27 01:50:04 +02:00
jschoubben 0f7f628730 ADR 0120: note the shared/region interaction with hq 128
A roster fact may be shared — written into a marked region of the machine's
file (into: block, hq 128) rather than as the whole file. The template
renders the content; shared decides how the host lays it down. Composes with
hq 128: the region mechanism is the host's, the format is the module's.
2026-09-27 01:42:00 +02:00
jschoubben 970da74136 1.7: the composer exists and the composition does not
Correcting a tick and a claim I made one commit ago. 4.1 does not wait on an
enrolment user per live token; it waits on the whole composition, of which that
user is one input.

Tasks 1.3 and 1.4 are honest about what they built — the composer, the
derivation, the permission model, the stream and consumer definitions, the
asserter, all pure and held by unit tests and a golden composition. Nobody wrote
the caller. Measured: outside the package that defines them there is not one use
of the composer, the permission derivation, the stream set, the stream asserter or
the principal type. Step 1's "done when" claims every account and permission
composed from the manifests, and a mesh raised today would stand up a server with
no user list at all.

It also needs state the mesh does not keep. Design 25 §4 says the file holds
bcrypt hashes and that passwords are minted and sealed exactly as today — but
today the mesh mints one, hands it to the broker through a management call, seals
the plaintext to the holder and keeps nothing. With no management call the hash
has to survive every later recomposition, because the first thing a new module or
a person's access change touches is a file that must still hold every other
user's password. No bcrypt hash is stored anywhere in the controller.

Named as its own task rather than folded into 1.3, so the gap between "the parts
of step 1 exist" and "the mesh does any of it" is visible.
2026-09-27 01:39:20 +02:00
jschoubben 4d4012cdf6 ADR 0120: a roster fact carries its format as a template; rewrite to-be 29 around it
The facts mechanism formatted the roster in Go in the control plane — one
formatter per fact, in the consumer's own configuration language. ADR 0120
makes a fact a path and a template: the mesh owns the data, the module owns
the format, and the control plane holds no format at all.

to-be 29 (operator accounts + what lives under a home) is rewritten to ride
it: the ssh files become roster templates, the whole ~/.ssh is owned with a
found/owned boundary that cannot lock the operator out, keys are mesh-owned
through an SSH CA (existing keys adopted not regenerated, the operator's
personal key signed not minted), and the ssh-agent is a user-scoped service.
2026-09-27 01:33:12 +02:00
jschoubben 0a61531c42 WBS 3.5 done; 4.1 waits on one thing, an enrolment user per token
All three halves of the host's link are through seams, and the reply address in
the payload is now proved from both ends rather than one — the test asserts the
transport's own field held the consumer's ack address by the time the request
arrived, so a server that stopped claiming it fails a test instead of letting the
reason become folklore.

Two things had to be built for the host to hear anything at all: a node's
declaration consumer, which only the controller may create, and the enrolment
user's inbox, which design 25 §6 names and the composer granted none of. Both
were silent gaps — a node with either missing looks correct and hears nothing.

What remains is a single piece: something that composes an enrolment user per
live token. On the old bus that account is made imperatively through the broker's
management API; here there is no management API, so issuing a token has to
recompose the server's configuration. It is the only thing between the two links
and a mesh raised on NATS from nothing, so 4.1 now says so.
2026-09-27 01:32:26 +02:00
jschoubben 4b0b659084 Merge pull request 'ADR 0119: a taken tunnel's predecessor is retired once the take is proven' (#143) from decision/0119-a-taken-tunnels-predecessor-is-retired into main 2026-09-26 22:59:15 +00:00
jochen 0042ca9258 0119: link ADR 0118 now that it is on main 2026-09-27 00:58:55 +02:00
jochen 63c19456b4 0119 review: rollback needs the private network unassigned first; a configuration written back is retired again with the first original kept; only the interface's own file, never a link 2026-09-27 00:58:54 +02:00
jochen 2f195d501e to-be 08: the found tunnel's configuration is retired once the take is proven (ADR 0119) 2026-09-27 00:58:54 +02:00
jochen 8a78ff4efe ADR 0119: a taken tunnel's predecessor is retired once the take is proven 2026-09-27 00:58:54 +02:00
jschoubben 762300a380 Merge pull request 'issue 130: undeclaring a service stops it, even one the mesh only reloads or keeps running' (#142) from issue/130-undeclaring-a-service-stops-it into main 2026-09-26 22:58:43 +00:00
jochen 248c99ca6c 0118/130: link ADR 0117 now that it is on main 2026-09-27 00:58:17 +02:00
jochen 13208f0f48 0118: give a unit back the state it was found in — never-stop broke the converge rollback; process removal found and fixed 2026-09-27 00:58:17 +02:00
jochen ebd19c4c6c ADR 0118: undeclaring removes what the mesh made, gives back what it changed, leaves the machine's units as they are — resolves issue 130 2026-09-27 00:58:17 +02:00
jochen dd4cbabffb 130: ADR 0117 named, not linked, until it is on main 2026-09-27 00:58:17 +02:00
jochen 5b76a09da6 issue 130: undeclaring a service stops it, even one the mesh only reloads or keeps running 2026-09-27 00:58:17 +02:00
jschoubben a363a605cb Merge pull request 'issue 129: nothing makes a machine trust the mesh's own certificate authority' (#141) from issue/129-nothing-makes-a-machine-trust-the-meshs-authority into main 2026-09-26 22:57:30 +00:00
jschoubben 24835ab710 Merge pull request 'issue 128: the machine's hosts file is written whole, and on a workstation it is shared' (#140) from issue/128-the-hosts-file-is-written-whole into main 2026-09-26 22:57:02 +00:00
jschoubben ba397d4cbe Merge pull request 'ADR 0117: a machine's uplink is a seat — the mesh configures the manager, never the link' (#139) from decision/0117-the-uplink-is-a-seat into main 2026-09-26 22:56:27 +00:00
jschoubben f555d523c7 WBS 3.4 is done both halves; issue 127 holds 4.2, 4.3 and catch-up
The controller's inbound is through a seam with both transports behind it, and
the store window is now the server's rather than the controller's memory. Seven
claims about that were asked of a running server rather than reasoned.

Wiring the controller's own subscription is what found issue 127: every event
name in the catalogue is still written the way a routing key is, so design 29's
derivation turns a consumer's declaration into a subject no emitter publishes.
Thirty-seven manifests, one that cannot be composed at all. It fails on the first
mesh raised on the new bus and not before, which is why nothing had caught it —
the conformance fixtures pin one emitter against one subject, and both halves of
that pair are correct.

The node-facing flows are unaffected: those subjects are the mesh's own and
derive from nothing a module declares.
2026-09-27 00:55:07 +02:00
jschoubben 4f93d304d7 WBS: a person's account is done; the client is not blocked by step 3 2026-09-27 00:18:05 +02:00
jschoubben d898bd87e8 Step 5.4 was wrong from 0119 onward; removed
It waited on a retirement condition 0119 abolished when it made the
deprecated broker an ordinary provider. A step waiting for a condition
nobody set would sit open forever.
2026-09-27 00:16:45 +02:00
jschoubben 7e4da874a9 Design 25 §2: the eaten reply address is verified, not assumed
A claim the whole enrolment handshake rests on, now measured against a
running server rather than reasoned from documentation — and held by a test
so it cannot become folklore if a server version changes.
2026-09-27 00:15:00 +02:00
jschoubben 53092020eb WBS: asking a tool is through the seam; a build is a different shape 2026-09-27 00:11:44 +02:00
jochen df4a3538c3 0117 review: a holder's service declares no state — the manager's lifecycle is the machine's; a start-only setting's gap on a fresh machine 2026-09-27 00:07:42 +02:00
jschoubben 00817fb9e3 Design 25: the store window, and what moving it into the server changes
The guarantee is the same and the mechanism is simpler — a nak with a
delay, no parked list, nothing lost when the controller restarts. It costs
one thing: a naked message comes back whatever happened meanwhile, so an
older report is redelivered after a newer was applied. A report already
carries the digest of the declaration it answers, so supersession becomes a
check rather than memory — ordering settled by what a message says, not by
when it arrived.
2026-09-27 00:06:15 +02:00
jochen 90b8eeff6b 129 review: the example name is a routed name, not a doubled suffix 2026-09-27 00:03:36 +02:00
jochen a865fc7d79 128 review: located; the fix as built (block, at, never held, order) 2026-09-27 00:03:30 +02:00
jochen c3313f6e17 0117 review: seat table row + decision cited, networkd/dhcpcd lines match the modules, block placement, dispatcher scope, references 2026-09-27 00:03:15 +02:00
jschoubben 9946e852e1 WBS: 3.5's outbound half is in 2026-09-27 00:01:56 +02:00
jochen 60a53f9b18 issue 129: nothing makes a machine trust the mesh's own certificate authority 2026-09-26 23:58:48 +02:00
jochen ee31f9f761 issue 128: the machine's hosts file is written whole, and on a workstation it is shared 2026-09-26 23:58:48 +02:00
jochen 0e066473b3 ADR 0117 accepted; a manager that cannot reload takes the setting at its next start (dhcpcd, measured) 2026-09-26 23:58:48 +02:00
jochen 504adef221 ADR 0117: a machine's uplink is a seat — the mesh configures the manager, never the link 2026-09-26 23:58:48 +02:00
jschoubben 9f6aa7ea9c The bus is the mesh's centre, not a transport that replaced one
Two things. A paragraph from the superseded 0117 survived beside the 0119
correction that reversed it, so §5 said both that the amqp interface
retires and that it does not. The stale one is gone.

And the framing. §1 opened with "the bus carries five kinds of traffic
today, and this design keeps the five", with a column mapping each to the
queue it used to be — which describes the mesh's nervous system as a port
of something that did a fraction of this. It now says what the bus is: a
role addressable without knowing its holder, the mesh's own state, work
that queues until somebody can do it, and permissions derived from what a
module declared. Conditions, observation and a person's client land there
too as they are built.

Glossary gains `bus` and `the deprecated broker`, with a note on why not to
say "compatibility broker" or name it after a protocol — the second invites
exactly the backwards framing this commit removes.
2026-09-26 23:54:09 +02:00
jschoubben 8deecf620a Merge pull request 'to-be 29: a node has operator accounts, and the mesh owns what lives under a home' (#138) from design/29-a-node-has-operator-accounts into main 2026-09-26 21:53:39 +00:00
jschoubben 64bbdc30c1 to-be 29: a node has operator accounts, and the mesh owns what lives under a home
The mesh models machines but not the people on them — a node record
holds no username, and no module places anything under a home. So who
you are on each node (jochens/ace/jochen) is unknown to the mesh, and
nothing owns ~/.ssh, dotfiles or ~/.config. HAL knew it; the nox mesh
dropped it. Proposes the account as a node fact and a home-scoped
resource class (the ~/ mirror of ADR 0112's /var/lib placement), with
the login key staying the operator's (ADR 0051). Not urgent — HAL's
generators still run — load-bearing at node-by-node retirement. Found
generating ~/.ssh/config from HAL's registry, which nox has no
equivalent for.
2026-09-26 23:53:13 +02:00
jschoubben 672c994afa WBS: 3.4's seam is in, outbound half through it 2026-09-26 23:47:40 +02:00
jschoubben 2f9bb73685 WBS: the first fixtures are in, and what byte-for-byte means 2026-09-26 23:41:15 +02:00
jschoubben 5c193b3f54 WBS: 3.8's check is written 2026-09-26 23:34:42 +02:00
jschoubben fbf9440e8e WBS: 3.7 and 3.8 done, with the one check 3.8 still owes 2026-09-26 23:34:04 +02:00
jschoubben 9510bf5311 WBS: 3.2 done 2026-09-26 23:33:10 +02:00
jschoubben d940e14ec8 Design 19: the protocol on NATS
Task 3.2. ADR 0074's model is untouched — floor plus capabilities, partial
implementations legitimate, identity from the credential, dedup on
x-event-id, conformance as executable fixtures. The transport beneath it is
rewritten: exchanges and queues become subjects and streams.

Statements marked *verified* were checked against a running server while
the runtime's client was written, not reasoned from documentation. Three
of them are things the specification would otherwise have got wrong:

- the payload is the body alone, with metadata in NATS headers; an
  implementation that nested the whole envelope would agree with nobody
- a durable name may not contain a dot, while the ack subject joins two
  names with one — conflating them looks right in a permission list and is
  refused as a consumer name
- a certificate must carry a name the bus is dialled by, because the NATS
  client has no hook to replace hostname verification the way pinning did
  on AMQP

And one limitation lifts: a module may now call another's tool. Issue 049
recorded that a scoped account could not declare the reply queue a caller
needs, and ADR 0095 routed every ask through the control plane because of
it. Per-account inbox prefixes plus allow_responses replace that. ADR 0095
is not reversed — the control plane is still how a person asks — but
module-to-module calling stops being a question about capability and
becomes one about policy, which `uses` already answers.
2026-09-26 23:32:48 +02:00
jschoubben 80456981be WBS: 3.6 done, and the certificate constraint it surfaced
The NATS client has no checkServerIdentity hook, so pinning no longer makes
the name check redundant — the bus's certificate must carry a SAN matching
the address nodes dial.
2026-09-26 23:29:56 +02:00
jschoubben d2ed3152d3 Seat renames done; 0118 was wrong that it was a migration
A holding is derived at resolution from manifests, never stored, so there
are no recorded old names to rewrite. The work is an edit plus a kept rename
table — kept because a module lives in its own repository and may be
registered long after the catalogue stopped using an old name.
2026-09-26 23:06:55 +02:00
jschoubben 24d99ddd24 WBS: 3.9 done, and 1.4's client with it 2026-09-26 22:28:59 +02:00
jschoubben b5730525fe Merge pull request 'issue 127: a declaration that shrinks to empty is skipped, not sent' (#137) from issue/127-shrink-to-empty into main 2026-09-26 20:25:19 +00:00
jschoubben bb334e138b issue 127: a declaration that shrinks to empty is skipped, so the node keeps what it should drop 2026-09-26 22:24:58 +02:00
jschoubben b759e36bfd Design 29: tools, not serves; WBS 3.9 partly done
The manifest already uses serves for a provision's facts, so a module's
tools take their own key. Declaring them is itself new — until now a
module's tools existed only in a runtime environment variable.
2026-09-26 22:17:17 +02:00
jschoubben 0b8e84334f Why module events share one stream, checked against the server
Storage is not a property of a subject — a stream is a separate object that
covers one — so the question is always how many streams, not which topics
are durable.

Three facts decide it, two of them verified rather than assumed: NATS
refuses overlapping streams instead of merging them, so a shared stream
plus a per-module one is not available at all; a filter cannot express an
exception; and a stream per module turns one cross-module consumer into one
per module. So one stream, with per-subject caps for the fairness that
matters. Per-module age is genuinely unavailable, and a module that needs
it declares a seat.
2026-09-26 21:49:55 +02:00
jschoubben 39c802cbd4 Step 2: adoption recreates the bus once, on purpose
2.3 was already true and is now proved — the seat refusal is generic, and
three tests pin what matters: a second bus is refused by name, a different
bus implementation is refused too (which is what makes the bus replaceable),
and the AMQP broker no longer contends so both run on one mesh.

2.1/2.2 turned out not to be a no-op. The host keeps a container only when
its spec matches exactly; genesis raises the upstream image and the module
declares the mesh-built one carrying the entrypoint, so assigning it
recreates the container. That is ADR 0067's pivot and it is safe only
because the bus carries nothing yet — which is why step 2 comes before
anything speaks NATS. After it, never again: the config is a directory
mount, so accounts change without touching the container's spec.
2026-09-26 21:40:02 +02:00
jschoubben 86a084b7ff The mesh bus is required, not ambient
Design 29 said no module requires the bus. The catalogue disagrees: 49 of
72 modules take a broker credential and 23 do not, so an ambient connection
mints an account for a third of the catalogue that never speaks — and the
49 each hand-write the path it lands at, which is provisioning done badly
by hand.

The bootstrap argument that made it ambient was narrower than it looked.
"A provisioner needs an account before it can run" is true of a provisioner
process and says nothing about a provision the controller answers, and the
controller is not waiting on a bus account to compose one.

So: the mesh-broker seat delivers mesh-bus; a module requires it and gets an
address, a sealed credential and the trust to verify the server; a module
that requires nothing has no account at all. The requirement delivers the
connection, the declarations shape the authority, and declaring a subject
without requiring the bus is refused as incoherent.

mesh-bus and nats are deliberately two names: a module may run its own NATS
as a backing service exactly as one provides amqp, and a manifest saying
"nats" would otherwise mean either the mesh's nervous system or a private
queue.

The seat's Delivers was wrong twice today — amqp, then empty — and the
comment says so rather than reading as though it were always right.
2026-09-26 21:17:21 +02:00
jschoubben 85f972749a AMQP is a provision, not the bus
0117 went a step further than it had grounds for. It was right that the bus
is the only bus, and wrong that the amqp interface must therefore retire —
because it conflated two reasons to want a broker. Using one to reach
another module is a second bus and stays refused. Needing an AMQP broker as
a backing service, the way something needs a database, is ordinary, and
forbidding it would make the mesh unable to run normal software while
calling that architecture.

So the broker becomes a plain provider module: no seat, not foundation,
never raised at genesis, no retirement condition. lavinmq now claims nothing
and provides amqp; nats claims mesh-broker and provides nothing.

The rule that survives is about direction, not software: inter-module
communication goes over the bus. A module may hold a broker for itself; it
may not use one as a channel to another module. That is a review judgement
where 0117 could have used a parser, which is the honest cost.

0106's progressive insight was itself wrong and is corrected by a second one
there — nothing moves off the old broker, so its "one purpose" sentence does
not become true, it is just not what that server is.

The insight check needed two fixes it found itself: a date may carry
trailing words, and a bold run with a link is discussing an insight rather
than marking one. All four bad shapes still fire.
2026-09-26 21:08:40 +02:00
jschoubben 7abb268de6 Step 1 done but for its bed
1.1 to 1.4 built and tested. 1.5 turned out to need no controller change:
it already resolves the broker by seat and names no broker module in its
source, which is what ADR 0079 was for. The genesis module set naming is
scenario and installer config, carried with the bed.

Recorded what must NOT change yet: the amqps:// credential shape and the
5671 default are correct until the rollout, because steps 1-4 leave every
node on AMQP.
2026-09-26 21:03:35 +02:00
jschoubben 78a2274baf Designs 25 and 29 disagreed about the subject space; implementing found it
29 put a module's events and tools in one namespace, 25 kept mesh.events.*
and mesh.tools.*. One namespace is right — a module's authority over its own
name becomes a single pattern the server enforces — but it needs a kind
token, because a stream is a subject filter and mesh.mod.*.> would persist
every tool call in the mesh. Tools stay on core NATS for the reason 25
already gives.

So: mesh.mod.<module>.event.<name>, .tool.<name>, and seats the same shape.
2026-09-26 21:02:18 +02:00
jschoubben 3f9b316015 Design 25: a scoped inbox needs allow_responses, or nothing can answer
Found composing the first real configuration. Scoping every inbox to its
owner is right and leaves a responder unable to reply, because the answer
goes to the caller's inbox. The fix is not a wider grant but the server's
own allow_responses: one reply to the subject of a message the user actually
received. Without it every tool call times out while the permission list
looks correct.
2026-09-26 20:58:34 +02:00
jschoubben e05825a881 Design 29: versioning, provisioning and secrets on the bus
Versioning: additive is free; a breaking change is refused while callers are
bound, and the refusal names them, because the mesh already holds the uses
graph; a real break versions the subject, not the seat name, so the role
does not fork; binding is a recorded pin, not a drift to whatever is newest.
Semantic change stays open — no fingerprint sees it, and saying so beats
implying the check is complete.

Provisioning: a provisioner's create/remove/holds IS a serves protocol, so a
provision interface is a seat that also delivers a credential — which is why
design 26 already allowed that. The per-consumer resource is what stops the
two collapsing into one.

Secrets: sealed, so the bus is never trusted with plaintext — but sealed is
not enough, because a stream persists and a durable ciphertext is an archive
the day a key leaks. So a secret never enters a stream: core request/reply
only, and a declaration names a secret rather than carrying one, which is
0098's fetch-don't-store applied where carrying is worst. The vault's own
credential and the bus's own accounts are the two bootstrap exceptions,
resolved the way 0067 resolves the control plane.

Also rewrote the addresses paragraph, which was too compressed to follow:
on-bus addresses disappear because nothing stores them, off-bus ones are
untouched and still 0098's problem, and the bus's own address is the one
that cannot be a subject.
2026-09-26 20:44:16 +02:00
jschoubben 7b4916e9ec Modules declare their own seats; the mesh reserves mesh-*
The architecture 0117 opened needs a module to offer a service as a role on
the bus — one holder, addressed by what it does. A closed table in the
controller cannot express that: a capability a module contributes would
require changing the mesh itself.

But 0110 closed the set for a good reason — nothing could say what seats a
mesh had, and the hand count came out at eleven of thirteen. That argues for
enumerable, not hardcoded, and 0110 weighed free-form against a fixed table
without considering a third option: closed at any moment and derived from
the catalogue. A derived list cannot drift, which is how the count broke.

So: the mesh's seats stay the mesh's, reserved by the mesh- prefix so the
prefix is the rule and there is no list to maintain; ten seats are renamed
to restore 0079's convention; everything 0110 decided about what a seat IS
survives untouched.

Design 29 carries the declaration model: three namespaces, subjects derived
from local names so a manifest survives the wire changing, queues never
declared, five relationships (the job and state shapes 0041 had no room
for), and the build-publish-deploy lifecycle with hard, soft and build-time
dependencies distinguished.

0041 gets a progressive insight: "no per-consumer setup, only a
subscription" was a fact about a topic exchange, and a JetStream durable
consumer is a real object someone creates.

WBS 1.3/1.4 were wrong and say so: streams come at registration and
consumers at assignment, so only the foundation set belongs at genesis.
2026-09-26 20:34:32 +02:00
jschoubben 3f14264c7b Merge pull request '112 is fixed: a carried peer is nameable, and the resolver answers for all four machines' (#136) from issue/112-fixed into main 2026-09-26 18:13:53 +00:00
jschoubben 25d599094f 112 is fixed: a carried peer is nameable, and the resolver answers for all four machines 2026-09-26 20:13:37 +02:00
jschoubben b5b68e8852 Design 25: a host directory bind, not a named volume
Issue 115 is resolved and converted four modules away from named volumes;
the bus's own data is not the place to bring one back. Also: NATS carries
TLS on the client port rather than beside a plaintext one, so there is no
5671/5672 pair to mirror.
2026-09-26 19:34:54 +02:00
jschoubben 814c9e563f The bus is the only broker; step 1 starts
A module does declare requirements the provisioner fulfils — but the broker
it gets that way is a private vhost, the analog of a database, not the
mesh's bus. Two modules of the new mesh depend on it, so the compatibility
broker was never single-purpose and its retirement would have stranded them.

NATS is the heart: one bus, a module's messaging is subjects on it scoped by
what it declares, and no module is handed a server of its own. The seat
delivers nothing; the interface retires with the broker. Also closes the
EVENTS question — one stream, on the bootstrap argument, not preference.

Designs 25 and 28 go in-progress: step 1 is starting.

The insight check caught a false positive on its own first real use — its
bold-run pattern crossed newlines and joined an unrelated `**` to the
marker. Constrained to one line, still catching all four bad shapes.
2026-09-26 19:26:07 +02:00
jschoubben db4ca9b043 Merge pull request 'The bus in five steps: a decomposition, a breakdown, and progressive insight' (#134) from feat/the-bus-in-five-steps into main 2026-09-26 17:05:43 +00:00
jschoubben 5a6d0e111d Merge remote-tracking branch 'origin/main' into feat/the-bus-in-five-steps
# Conflicts:
#	02-DECISIONS/README.md
2026-09-26 19:05:36 +02:00
jschoubben c94e2ece53 Merge pull request 'Give 0115 a home, and match its batch's status — main is failing both checks' (#135) from fix/0115-checks-on-main into main 2026-09-26 17:04:50 +00:00
jschoubben 0af6479b2c Give 0115 a home, and match its batch's status
Both repository checks fail on main. 0115 is cited by no design, and it is
marked accepted while resting on 0112, which is proposed.

Design 27 already states the rule the record decides — "a module is assigned
at most once to a node, and that pair is the assignment's identity" — so it
is the home, and now says so. And 0112, 0113, 0114 and design 27 are all
proposed: the batch is under review, so the record is too. Promoting 0112
instead would be marking a record accepted to satisfy a check, which
check_rests_on names as a failure this repository already made once.
2026-09-26 19:03:14 +02:00
jschoubben 77a1493df4 Renumber to 0116: another record took 0115 on main
PR #133 landed a different 0115 while this branch was open. The bus record
is now 0116, with every citation in designs 19, 25, 28 and the index
following it.

Note: cycle.py and records.py both fail on main as merged, on that record —
nothing cites it, and it rests on 0112, which is still proposed. Both
pre-date this branch and are left for their own change.
2026-09-26 18:56:34 +02:00
jschoubben 21b54ee52f Merge main: 0115 was taken by another record 2026-09-26 18:54:59 +02:00
jschoubben 3950c2b75d Merge pull request '0115: one assignment of a module per node — the requirement is dropped' (#133) from decision/0115-one-assignment-per-module-per-node into main 2026-09-26 16:53:12 +00:00
jschoubben 9516d31a62 0115: one assignment of a module per node — the requirement is dropped, the module's name is the identity 2026-09-26 18:52:59 +02:00
jschoubben 1c808898a5 Allow progressive insight, and apply two to the bus record
A record can assert a fact that goes stale while the decision it supports
stays right. Superseding for that buries a sound record under a second one
and makes every reader work out which is live. So a correction of fact is
now made in place, marked and dated, with the old wording quoted — bounded
by three conditions and checked by records.py, which fires on an unmarked,
undated or back-dated note. Judgements still supersede.

Applied to 0115: no conformance suite exists to recapture, and the full
genesis bed cannot run until the links exist. Designs 25 and 28 follow.
2026-09-26 18:39:38 +02:00
jschoubben 6ab113e6c6 Break the bus work down, measured, in dependency order
Counting the surface first changed the plan twice: the genesis bed cannot run
until the links exist, so it belongs to step 4, and there is no conformance
suite to recapture — step 3 builds one against the current bus before moving
it. Both corrections are recorded in the breakdown rather than edited into
ADR 0115. Also indexes design 25, which was never listed.
2026-09-26 18:24:14 +02:00
jschoubben fe0c1e9da2 Divide the bus work into five steps, each proved on its own
The NATS change was recorded as one undivided item, which hid three gaps:
a mesh already running had no adoption path, the protocol specification did
not know its transport was being replaced, and nothing was runnable until
everything was. Dividing it is what surfaced them.
2026-09-26 18:19:56 +02:00
jschoubben dc80116b09 Merge pull request 'to-be 27: the three gaps answered' (#132) from design/to-be-27-gaps-answered into main 2026-09-26 15:44:53 +00:00
jschoubben db142ffa26 to-be 27: the three gaps answered — root is a node setting, the mesh's writes need no module-visible reservation, resolution is the controller's at composition 2026-09-26 17:44:38 +02:00
jschoubben e38814962d Merge pull request 'Issues 125 and 126: two apply-layer gaps the novox session hit live' (#131) from issue/125-126-from-the-novox-session into main 2026-09-26 15:25:41 +00:00
jschoubben 7842457d4b Issues 125 and 126: two apply-layer gaps the novox session hit live
125: a hold is not a line in the apply report — sixteen resources held
for an untaken module while four surfaces reported success, and the
operator stopped the edge's predecessor on their word (the route-proxy
flip outage). 126: a changed volume path neither recreates a running
container nor warns, and a roll-out upgrade policy makes a build a
deployment — together they turned a data-path migration into a forge
outage (the /var/lib move). Filed as 119/121 in the novox session
before syncing; renumbered past the other session's 119-124.
2026-09-26 17:25:27 +02:00
jschoubben 782d5ace04 Merge pull request 'Issues 119 and 122: what a host path is, and what a node's layout would replace' (#130) from issue/119-122-what-a-host-path-is into main 2026-09-26 15:21:34 +00:00
jochen f709e8e0fb Issues 119 and 122: what a host path is, and what a node's layout would replace
Not every host path names this machine. A system file the mesh owns is at that path on every machine
of the kind — the path is the fact. The operator's shared data is already answered as an access. A
path inside a container is the software's contract. What is left, and what a node's default layout
would replace, is 514: a module's own data, and what the mesh writes for that module.

Documents the reservation model as the records already have it — a root per node, one directory per
assignment, a placement for adopted data — and the three things nothing states: where the root comes
from, what sits beneath it, and the order the 514 are retired in.

Stops there deliberately. Changing where a definition looks without moving the data does not fail: the
mesh creates the directory, the container starts, the service comes up empty. Retiring these is a data
migration with a verification step, module by module, and belongs with whoever can see the machine.
2026-09-26 17:21:14 +02:00
jschoubben c5e1a9a8d5 Merge pull request 'Issue 122: count host paths, not paths' (#129) from issue/122-host-paths-not-container-paths into main 2026-09-26 15:11:33 +00:00
jochen c4d9b515ea Issue 122: count host paths, not paths
A path inside a container is not a fact about the machine — /run/secrets and the directory a server
keeps its data in are the software's own contract, true in any mesh that runs it. Only the host side
of a mount names where it landed.

The first sweep matched path-shaped strings, so it counted both halves of every mount and every
in-container location a value mentioned: 798. Counted by role — directory and file resources, the
host side of mounts, accesses, and the targets of binds, grants, receives and secrets — it is 698
across 70 definitions.
2026-09-26 17:11:18 +02:00
jschoubben 1012fff607 Merge pull request 'Issue 122: count it properly — 30 of 71 definitions name this installation' (#128) from issue/122-the-census into main 2026-09-26 15:09:17 +00:00
jochen d5a4cb0d4f Issue 122: count it properly — 30 of 71 definitions name this installation
The five modules this report first named were what a first look found. A sweep of all 71: 49 public
domains across 26, the node's own name 58 times across 19, a routable IP 12 times in one, 798
absolute paths across 70 (issue 119's number, grown), and no email addresses at all.

The sharpest case is not a domain: a mail module states the node's public IPv4 as the address it
trusts a real-IP header from, so a node that moves or gains a second address stops attributing mail
correctly, silently. Two upstream resolvers are excluded deliberately — naming a public DNS service
is a policy default, true of any mesh, not a fact about this one.
2026-09-26 17:09:01 +02:00
jschoubben 5db79cd168 Merge pull request 'Issue 124: a consumer cannot be told a value its provider derived for it' (#127) from issue/124-a-consumer-cannot-be-told-what-its-provider-derived into main 2026-09-26 14:47:25 +00:00
jochen 146fd6b3a8 Issue 124: a consumer cannot be told a value its provider derived for it
The object store derives each consumer's bucket from the login the mesh minted, and never reads the
one a definition named. The consumer still has to tell its own software which bucket to use, and has
no way to be told: bound values come from the provider's serves, which is a literal block identical
for every consumer, and a provisioner returns nothing. So all three consumers wrote the answer down
by hand and one of them wrote the predecessor's bucket — a key scoped to one bucket and software
asking for another, which reads like a credential fault and is not one.

Records the general shape: any interface where the provider names the resource forces the consumer to
reproduce the provider's rule, kept in agreement by hand and checked by nothing.
2026-09-26 16:47:06 +02:00
jschoubben 007e3f4b03 Merge pull request 'Issue 123: the image registry is named after a role, and *artifact* is defined as one format' (#126) from issue/123-the-image-registry-is-named-after-a-role into main 2026-09-26 13:38:00 +00:00
jochen 3abb3a3c07 Issue 123: the image registry is named after a role, and artifact means one format
Three wordings disagree, and the confusion is the damage: the glossary defines artifact as an OCI
image while the build vocabulary already names four kinds in use, two of which are not images; the
image registry's seat is named after its job while ADR 0079 names foundation seats after their servers
and ADR 0109 names package seats after their ecosystem; and prose that says 'the module's image' reads
as though a module were an image.

Records the question the naming hides: ADR 0075 keeps two provisions because packages and images are
two protocols, and already allows the forge to provide the artifact store. The second implementation
rests on a bootstrap argument, and the forge has the same upstream-server shape the store and broker
have, which ADR 0078 raises as plumbing and adopts in place.
2026-09-26 15:37:41 +02:00
jschoubben 112963524f Merge pull request 'Issue 121: retract step 4 — the forge's service is not built' (#125) from issue/121-step-4-retracted into main 2026-09-26 13:36:36 +00:00
jochen a1d8b478ec Issue 121: retract step 4 — the forge's service is not built
Step 4 claimed the forge cannot exist as a container before the builder has built its image. The
forge's server is an upstream public image pinned by digest; only its runtime sidecar is built. The
store and the broker have the same shape, and ADR 0078 raises both at genesis as plumbing and adopts
them in place — so the forge can be raised the same way and serve git, packages and OCI before
anything is built.

What survives: a grant is minted by the provider's runtime sidecar, which is built, so the question
is whether raise-service, grant, build-sidecar simply works. Sequencing inside the mesh, not images.

The wrong version came from taking a record's bootstrap argument at face value instead of comparing it
to how the store and broker are raised — one command away in the manifests.
2026-09-26 15:36:19 +02:00
jschoubben 1489d17238 Merge pull request 'Issue 108: the second door was attached to the wrong thing' (#124) from issue/108-one-door-after-all into main 2026-09-26 13:22:55 +00:00
jochen c343fc68c5 Issue 108: the second door was attached to the wrong thing
Two things were treated as one. The mesh's own artifact store holds the store seat and is internal by
design — reached by name over the overlay, no accounts, ADR 0082. Serving a registry publicly is a
service the mesh can host: a module with its own name, accounts and storage, like anything else it
runs for somebody. The conversion this report was written beside gave the seat holder a second public
door over the same filesystem, which is neither.

Keeps the original issue whole — no garbage collection, and the settings a routine needs are not
enabled — drops the two-door complication, and sharpens one thing: deletion on the only door is
deletion on a door with no accounts, which the predecessor kept behind its authenticated one.
2026-09-26 15:22:09 +02:00
jschoubben 859ff49ff2 Merge pull request 'Issue 122: the pattern is already on main, in five modules' (#122) from issue/122-the-instances-already-on-main into main 2026-09-26 13:07:42 +00:00
jochen e399a2c148 Issue 122: the pattern is already on main, in five modules
Asked whether merging the three reviewed changes would set a precedent. It would not: five modules
already carry a name belonging to this one mesh — a workflow module stating its host, protocol and
absolute webhook URL, and two carrying a full clone URL for a repository on the mesh's own forge.

That changes what the issue is for. There is no version of this catalogue today that does not name
the mesh it was written in, so refusing three changes buys nothing and a mechanism is the only thing
that removes any of them. The three were merged on that reading, each PR saying so.
2026-09-26 15:07:22 +02:00
jschoubben bfcb660a8e Merge pull request 'Issue 122: a module cannot ask for its own public name' (#121) from issue/122-a-module-cannot-ask-for-its-own-public-name into main 2026-09-26 12:58:41 +00:00
jochen d7078061ea Issue 122: a module cannot ask for its own public name
Three open module changes independently wrote one mesh's names into the catalogue — two literal
public URLs, because the software generates absolute URLs behind a proxy, and one bucket renamed to
match what exists here. None was careless: the mesh composes <label>.<public-domain> for the proxy
and never hands it back to the module that asked for the route, and no interpolation yields a public
name, so writing the answer down is the only expressible option.

Files it rather than blocking the three, because the fix is a mechanism and the instances are live
needs. The cost is stated: a second mesh installing the identity provider gets the first mesh's
hostname, and nothing distinguishes a literal domain from a version number.
2026-09-26 14:58:18 +02:00
jschoubben bba9371832 Merge pull request 'The seats as they run, and to-be 26 implemented' (#120) from design/26-the-seats-implemented into main 2026-09-26 12:50:02 +00:00
jochen 3ff38d2ee1 The seats as they run, and to-be 26 implemented
Both halves are on their main branches, so the seats stop being an intention. Writes the as-is
document from the controller's code and the catalogue's manifests: the closed set of fourteen, the
three refusals a claim meets, the holder being an assignment and nothing else, and the one place a
seat changes resolution — which of several providers answers, never whether a requirement may go
unanswered.

Two things the as-is layer exists for are stated rather than smoothed over: a seat cannot answer
before it is held, which is the standing condition issue 121 records; and capacity is not
implemented at all, so the design's bench has no counterpart in the code.
2026-09-26 14:49:44 +02:00
jschoubben a89b0b5586 Merge pull request 'Issue 121 diagnosed: the seats work renamed the requirement, not the order' (#119) from issue/121-diagnosis into main 2026-09-26 12:48:12 +00:00
jochen 9766a3afce Issue 121 diagnosed: the seats work renamed the requirement, not the order
Asked first whether the seats change fixed this in passing, since it landed the same day and
touches both manifests the report names. It did not: the seat's holder is consulted only where
several nodes provide the thing, and with none providing it resolution refuses outright. Genesis
has no exemption — the unchecked first pass exists to learn what each node offers, and a
declaration is never built from it.

Records the part that did change: the three tests left failing on purpose were deleted by the
controller's seats PR and replaced with passing seat-based ones, so the gap is invisible again.
Adds the resolver to located-in, since that is where the refusal is.
2026-09-26 14:47:45 +02:00
jschoubben 8211dfd72c Merge pull request 'Accept ADR 0110 and ADR 0111; the seats design is in progress' (#118) from decide/0110-0111-accepted into main 2026-09-26 12:28:42 +00:00
jochen a7b2db9efc Accept ADR 0110 and ADR 0111; the seats design is in progress
The seats half of to-be 27's review is settled, so the two records it rests on are accepted and
the vocabulary catches up: the glossary's *seat* becomes a named role from a closed set, held by
an assignment and possibly delivering a provision, and 23 — Choosing a provider gains the seat
step in resolution, with ambiguity still refused rather than guessed. Both were held back when
0110 was proposed, because a document may not rest on a record that is not accepted.

26 — The seats moves to in-progress rather than designed: it names the files that implement it,
and naming a file claims implementation, which is only defensible once those files are on the
owning repositories' main branches. It becomes implemented when mesh-controller #63 and
mesh-catalog #69 land.

0112, 0113 and 0114 stay proposed; to-be 27 stays proposed with them.
2026-09-26 14:28:23 +02:00
jschoubben 99ffa6474d Merge pull request 'Issue 121: builder's real package-registry grant deadlocks a genesis bootstrap' (#117) from issue/117-builder-package-registry-deadlocks-genesis into main 2026-09-26 12:20:53 +00:00
jschoubben cb770f95c4 Merge pull request 'Issue 118: the analytics store answers the dial and times out the query' (#116) from issue/118-umami-store-query-timeout into main 2026-09-26 12:20:46 +00:00
jochen 09e502058b Issue 121: builder's real package-registry grant deadlocks a genesis bootstrap
Renumbered from 117, which is taken on main by 'a module's own code is a container in one record
and a process in another' — two reports claimed the same number and git would not have said so.

Scrubbed the node's name and a real registry path; this repository is public.
2026-09-26 14:20:05 +02:00
jschoubben 74b88d8efc Merge main 2026-09-26 14:19:58 +02:00
jochen b083790b21 Issue 118: the analytics store answers the dial and times out the query
Keeps 118: the other claimant to this number is on main as issue 119, where ADR 0112 points.

Scrubbed the service's public name — this repository is public — and completed the report's
frontmatter with the fixed-by and amended-design keys every other report carries.
2026-09-26 14:19:40 +02:00
jschoubben 0740de9d14 Merge main 2026-09-26 14:19:33 +02:00
jschoubben 79c692959e Merge pull request 'To-be 27 (proposed): a module requires, the mesh resolves — with ADRs 0109–0114, research 016 and issue 119' (#113) from design/27-a-module-requires-the-mesh-resolves into main 2026-09-26 12:17:56 +00:00
jochen d2044fb7b4 Merge main: the bus design, issues 113/114/117/120 and research 017 landed
# Conflicts:
#	04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md
2026-09-26 14:16:05 +02:00
jschoubben 01c6b89cc5 Merge pull request 'Design 25: the bus on NATS — proposed architecture for review; issue 103 resolved' (#93) from design/25-the-bus-on-nats into main 2026-09-26 12:15:19 +00:00
jschoubben 93502a05dc Merge pull request 'Issue 117: a module's own code is a container in one record and a process in another' (#110) from issue/117-a-modules-own-code-is-a-container-and-a-process into main 2026-09-26 12:14:38 +00:00
jschoubben 1272a97fd6 Merge pull request 'Research 017: a mesh that heals itself' (#115) from research/017-a-mesh-that-heals-itself into main 2026-09-26 12:14:24 +00:00
jschoubben de0c9b6cfc Merge pull request 'Issue 120: a provisioner remembers what it did, not what is there' (#114) from issue/120-a-provisioner-remembers-what-it-did-not-what-is into main 2026-09-26 12:14:17 +00:00
jschoubben 77934991c4 Merge pull request 'Issue 113 resolved by the repin, and what issue 064 did not cover' (#108) from issue/113-record-the-repin-and-fold-114 into main 2026-09-26 12:13:52 +00:00
jochen b5512bed32 Research 017: a mesh that heals itself
The operator's wish written as intended behaviour for the NATS bus:
every loop compares against what is, repairs by the ordinary path, never
destroys, and raises a condition for what it cannot fix. What is done
before NATS is limited to what survives the move.
2026-09-26 00:54:27 +02:00
jochen 0c5eac0218 0110 amends 0109: its seats are provisions, and moving npm takes a holdable seat 2026-09-26 00:44:35 +02:00
jochen b3bd50c588 Issue 120: a provisioner remembers what it did, not what is there
The harness compares against its own memory, so a backend that loses
what was provisioned (the cache's ACL users on a server restart) is
never provisioned again, silently.
2026-09-26 00:44:10 +02:00
jochen e387c4bd0e Apply review: two credentials, staged admin rotation, a ninth provider
The fact-check found mailu, whose user is its mailbox, so 0114 rotates
over two credentials rather than two logins, the adapter choosing what a
credential is. Also: minio keeps non-empty buckets; five backends take
their admin credential only at first init, so single-party rotation is
staged; postgres ownership moves to a non-login role; the harness keys by
consumer; rotation state lives with the vault. Consistency fixes across
0110-0113, 26 and 27; issue 103 resolved by mesh-host PR #22.
2026-09-26 00:38:06 +02:00
jochen 43f63ed41c ADR 0114: a two-party credential rotates over two logins
Graduates research 016. Retiring a login is separated from removing a
consumer, which closes a data-loss path in five providers; single-party
secrets rotate in place; the number of parties decides, not the provider.
2026-09-26 00:22:21 +02:00
jochen 942ebe350f Research 016: survey how each provider can rotate a credential
Overlap as drafted in 0113 would have deleted consumer data: seven of
eight providers name the resource after the login and five drop it on
remove. Rotation is now undecided in 0113 and to-be 27, pending the
survey. Also: a requirement naming a seat resolves to its holder, a
person chooses among remaining candidates at assignment, the controller's
secrets are requirements of its definition, genesis seals to the
control-node key, and moving the vault or broker is break-glass.
2026-09-26 00:14:23 +02:00
jochen 805df3f81e ADR 0113 and to-be 27: rotation overlaps old and new credentials
Decided with the author. A credential is never changed in place: each consumer has two logins, both
derived by the mesh, and uses one at a time. An applier adds the new login beside the old through the
adapter's existing create, and confirms both work; only then are readers released to the new one and
restarted by derivation; only when every reader has confirmed is the old login retired through the
existing remove.

It closes the three cases review found in applier-first rotation: an offline reader keeps working on
the old login until it returns; a bus account's owner keeps its bus until it has moved; a provisioner
restarted mid-rotation is still delivered both values. Nobody is ever without a credential that works,
which replaces to-be 13's all-or-nothing rule with a stronger one.

No consumer module changes. The alternation is the provider loop's. A provider's adapter gains one duty,
giving both logins the same rights over the consumer's data — in postgres, membership of one role that
owns it. The mesh derives two logins per consumer, both within ADR 0049's limit, which 0113 now names
among what it amends. Every rule has a check: overlap, offline reader, bus account, restarted
provisioner, equal rights, login length, and confirmation only once the old login is gone.
2026-09-25 23:50:20 +02:00
jochen 4a1b218706 Seats held by assignments, one assignment per module per node, and 0113's bottom of the stack
Decided with the author:
- A seat is held by one assignment, not claimed by a definition. A definition says which seats a module
  can hold; an assignment says which it does. The store module can run on every node and one assignment
  holds mesh-store; moving a role changes an assignment, never a definition. The foundation's seats name
  what the mesh itself uses and route no consumer — database and amqp consumers use co-location, the
  holder included. This replaces the wrong rationale that the foundation's store is "provider to nobody",
  which contradicted ADR 0078 and to-be 21. 0079's one-postgres rule becomes one mesh-store holder.
- A module is assigned at most once to a node. The instance identity in 0112 and 27 is withdrawn, and the
  login-length problem with it.

Review fixes to 0113:
- The bottom of the stack: the vault is installed as soon as the shared runtime base exists, and genesis
  generates everything needed until then — including the permanent controller's, the control-node
  agent's, the builder's and the broker provisioner's bus accounts, and the controller's store login.
  Genesis creates those accounts until the broker's provisioner runs and adopts them.
- Genesis's values are delivered recorded as the mesh's own, so 0092's never-replace rule for operator
  values does not make them unrotatable.
- Backend-issued secrets (a forge's once-only API token) enter through the vault. Non-module parties
  (the controller's logins, node agents' accounts) are answered the same way, the controller asking on
  their behalf; an enrolment token reaches the controller only as what verifies it.
- A secret with no provisioner to apply it is marked not rotatable by the mesh and refused, instead of
  a restart reported as done. Unused password generators in six provider clients are removed, and a
  catalogue scan checks no module mints.
- Rotation's lock-out cases (offline reader, bus account owner, restarted provisioner) are recorded as
  open, with overlap and re-confirm-with-safeguards as the two answers, to be chosen before acceptance.

0110, 0111 and 26 are marked proposed: they changed in meaning and are under review, and an accepted
record must not rest on proposed ones. To-be 23 and the glossary are restored to main; they change when
these records are accepted.
2026-09-25 23:47:26 +02:00
jochen 1b5f2c2c1a ADR 0113 and to-be 27: address the review of the vault rework
Two decisions taken with the author:
- Genesis delivers and the vault adopts. The vault cannot run first — it is built on the runtime base
  the installation makes after the store, broker and controller, and it learns its work over the bus.
  Genesis generates the foundation's first shared secrets, seals them to the operator key, and
  delivers them to the vault through the path an operator's value takes; from then on the vault holds
  and rotates them. This answers ADR 0085's own reason for rejecting vault-only minting, which 0113
  now names instead of stepping around.
- Rotation re-confirms on every pass. An applier repeats its confirmation until acknowledged, so a lost
  message costs one pass; an applier that stops after applying locks readers out until its supervised
  restart, and that window is stated and shown, not claimed away.

Fixes:
- Scope: a shared secret is made by the vault; a private key (node sealing keys, the operator's key,
  the certificate authority) is made where it is used. The inventory adds the makers the first version
  missed: node and builder broker passwords, and enrolment tokens.
- Broker accounts are created by the broker's provisioner, not the controller, so the controller never
  holds their plaintext; mesh-broker delivers amqp again — one broker per mesh — and only mesh-store
  delivers nothing.
- secret is a reserved provision: only the mesh-vault holder may provide it, and no pin routes around it.
- A secret's contract says whether a recipient applies it or reads it at start; appliers are never
  restarted for it, init-only secrets are applied, and confirmation is to-be 13's standard.
- Operator secrets are one rule everywhere: a secret requirement answered by the vault (0112 no longer
  says otherwise). A data provider's adapter may return fields; the data-return check names a lab consumer.
- 'Holder' now means a seat's holder only; a secret has recipients.
2026-09-25 23:27:58 +02:00
jochen fa2c09a2c5 ADR 0113 and to-be 27: the vault makes every secret — provisioning all the way down
A secret comes into being seven ways today: provider credentials, own secrets (54 modules), broker
accounts through a command that is easy to forget, a vault that only records what the controller
mints (6 modules), operator values, licences, and root secrets. The vault was built to end own secrets
and did not; the old path was never retired.

0113 is rewritten as a waterfall. The vault makes every secret and nothing else does. A provider that
needs a secret for a consumer requires it from the vault, declared once in its provision's contract
and expanded per consumer by resolution; the vault delivers it to both holders, each sealed to its own
node, so a provider's code is unchanged. Own secrets, broker passwords, operator values and licence
credentials take the same path. Genesis is not an exception: it raises the vault first and asks it,
so there is one way a secret is made from the first one on. The vault can sit at the bottom because it
requires nothing but a broker account.

One shared mint function in the SDK was considered and rejected: generation becomes uniform but custody
stays spread over every provider's machine, and each SDK language needs its own implementation.

Rotation is asked of the vault and is provider-first: the value goes to the holder that accepts it,
which confirms, before the holder that presents it gets it, so the lockout window shrinks to the
consumer's own restart, and an unconfirmed provider holds the rotation rather than half-doing it. The
host derives which processes to restart or recreate from the requirement a definition reads, so no
definition declares restart-on for a secret. A rotation shows unconfirmed until each consumer restarted
and passed its health check. Issue 103 becomes a prerequisite.

The file is renamed to match what it now decides. 0112 follows.
2026-09-25 23:10:52 +02:00
jochen 6e3373c879 Design pass: address the review
0113 — the plaintext claim was false under its own mechanism: handing a provider's answer to the
controller puts every secret on the broker and in the controller in the clear. The provider now seals
each secret field itself, to the consumer node's public key the mesh hands it, and the controller
carries sealed fields it cannot open. That is stricter than today, where the controller holds every
minted credential in the clear. Option 3 (plaintext to the controller) is recorded and rejected. The
foundation exception now covers root-secret rotation (0085) and forms like the broker admin's hash, so
no phase claims to remove the broker's bootstrap step. To-be 24 and 13 are named among what it amends.

27 — resolution is consistent with 0110: co-location and the only provider apply only where no seat
delivers the provision, so an unheld seat is refused even with one provider. The secret-field rule now
matches 0086 exactly (a declared env-file, never a container environment value). The seat placeholder
is the controller's, and the one module reading it moves to a host port. Contracts are held by the
controller and written down in phase 1, so they can be checked; every rule has a check. An operator's
secret is still the operator's, with the vault as custodian. Which seats a module holds is listed as
not settled.

0110 — the unheld-seat-with-one-provider case and the one-answer-for-everyone rule have checks; the
claim about moved manifests is corrected. 26 — the table governs and the code catches up, not the
reverse; scope and capacity agree with the glossary; moving a seat is described as it really is today.
0112 — aligned with 27, and lists 0049 and 26 among what it changes.

Issue 118 is renumbered 119: another branch took 118 first. 'Control-plane' is gone from 0110 and 0111.
2026-09-25 22:46:10 +02:00
jochen aad92ea8fe To-be 27 and ADR 0113 (proposed): a module requires, the mesh resolves
The design pass. Everything a module needs is a requirement: a name, a contract, and one of four kinds
of provider — a module, the node's host, the mesh, the operator. Installing a module resolves every
requirement or refuses, naming everything missing at once. It retires six mechanisms that grew
separately: provisions through bindings, settings, assigned ports, machine facts, minted secrets and
literals in the definition.

ADR 0113, proposed: a provider makes what it provides, and the mesh carries it back sealed to the
consumer's node. It is the return path ADR 0048 left "to a separate decision", now needed three ways:
data provisions with nothing to answer with, contracts needing a value the controller cannot make, and
a vault that generates nothing. Who a consumer is stays the mesh's (ADR 0049). Genesis is the one
exception. On acceptance it supersedes 0048 and amends 0085.

ADR 0112 is revised from three sources to that single concept.

ADR 0110 is amended for two points raised in review. The vault gets the mesh-vault seat (issue 106).
A seat's holder outranks co-location for a provision it delivers. Writing that down exposed an
inconsistency: mesh-store delivering postgres-database would have sent every database consumer to the
control-node, against to-be 23's node-local stores. So a seat delivers a provision only where the mesh
has one answer for everyone — artifact store, npm registry, git, vault — and mesh-store and mesh-broker
deliver nothing. 23 and 26 follow.

'Control plane' becomes 'controller' in the records written today.
2026-09-25 22:34:46 +02:00
jochen 7668190154 ADR 0112 and issue 118: address the review
- Secrets follow ADR 0085 as amended: a module's own secret is a provision the controller mints and
  the vault records. The previous commit had that backwards. Whether the vault should generate
  instead is recorded as an open question, not decided.
- A directory's contract is owner and mode only. The persistence flag was the keep flag ADR 0030
  refused; a directory is kept while it holds anything, and disposable data is a named volume (0107).
- An operator's shared data stays an access (ADR 0051), which rejected an operator-owned directory.
  Only where its path is written moves to the assignment.
- The records it changes on acceptance are named: 0051, 0091, 0046 (settings keyed by instance),
  0084 (a provider is a node and an instance), and the glossary, which gains its new words only
  when the record is accepted.
- How it is checked covers every stated rule. Container-side paths are no longer flagged by the
  host-path rule, and code fallbacks are covered.
- Provisions are what other modules provide. A seat's occupant is not listed as one, and the vault
  is not described as selectable per assignment.
- 'Control plane' becomes 'controller'. The provider count is ten of eleven, not eleven of twelve.
2026-09-25 22:29:38 +02:00
jochen 90fb7ae4ad ADR 0112: a module's own secrets are a provision from the vault, not something the mesh generates
The first draft listed minted secrets under what the mesh generates. ADR 0085 made a module's own
secret — a password, an internal token, an external key it was handed — a secret provision answered
by the vault, like a database by the store. What the mesh still mints is the delivery credential for
each provision a module takes (ADR 0048), the vault's own included.
2026-09-25 22:29:38 +02:00
jochen ca235e775f Issue 118 and ADR 0112 (proposed): a module definition names no node, no mesh and no path
Issue 118 records what a review of where module code reads its files found: 789 host-path strings
in 70 of the catalogue's 71 definitions, every one a decision the definition makes about a machine.
Mounts are checked (ADR 0091); the same paths retyped as values are not. It records what that has
already allowed — a DNS provider that would provision nobody silently, a contributions file that
names credentials by host path and so forces every provider to mount at the identical path, an SDK
loop that treats an unwritten contributions file as empty without a word, defaults in code that
disagree with their own manifests — and that no module can be assigned to one node twice, because
every identity is keyed by the module's name.

ADR 0112, proposed for review, answers it the way ADR 0038 answered ports: a definition names
variables, and installing it resolves every one or refuses, from three sources — the assignment's
own configuration, provisions the mesh resolves against a contract, and what the mesh generates or
knows. A directory becomes a provision: the module requires one by name with its owner, mode and
persistence, and where it lands is the assignment's. The mesh's own files stop carrying host paths.
An assignment gets an identity of its own, so a module may run twice on one node.

Checking copies for agreement was rejected as checking something that should not exist; rewriting
paths per assignment was rejected as inferring which strings are paths by their shape. Syntax, a
node's default layout, and when a second instance becomes possible are left to the design.
2026-09-25 22:29:38 +02:00
jschoubben 74ae0609cb issue 118: umami's store answers the dial and times out the query 2026-09-25 22:01:28 +02:00
jochen d94fe8f638 To-be 26: name the files that implement the seats and the build source 2026-09-25 20:47:48 +02:00
jochen c4cac767f8 ADR 0110: admit the-private-network, claimed by a manifest the control plane composes in code
The enumeration behind the first set read manifests in two repositories and missed a claim made
in the control plane's own code: the private-network module it ships claims the-private-network at
node scope. A closed set without it would refuse the control plane's own module. Thirteen claims in
use, naming twelve seats.
2026-09-25 20:35:31 +02:00
jochen dbe100ca96 ADR 0110 and 0111: a seat is a module assignment from a closed set, and a build source may live on the git seat
Seats have been doing two jobs and neither is written down. The mechanism ADR 0009 introduced is
enforced — a second holder is refused — but any well-formed name becomes a seat by being claimed,
and nothing can say which seats a mesh has or who holds them: holdings are assembled while planning
and discarded. The enumeration done while preparing this missed the control plane's own manifest,
because core modules' manifests live in its repository rather than the catalogue.

0110 closes the set. Each seat has a name, a scope, what occupying it delivers, and the record that
made it one; a claim outside the set is refused. A seat is held by a module assignment, and what the
mesh knows about the holder is what it knows about that assignment — nothing is stored beside it. A
seat may deliver a provision, and then its holder answers for it among several providers: pin, then
the holder, then the only provider, then refused. That keeps 0009's "refused, never guessed": the
seat is the choice made once, mesh-wide, instead of a pin per consumer node. The first set is the
eleven seats already claimed plus 0109's npm-package-registry, so nothing in use is refused.

Two concepts — seats for exclusion, a new word for consumable singulars — was rejected: both mean
"this mesh's one X", and the overview a person wants is one list.

0111 gives the mesh a git seat and makes a build source one of two explicit forms: a repository on
the seat's holder, recorded by its path and cloned from wherever the holder runs at build time; or
an external URL, recorded and cloned exactly as given. Recognising self-hosted sources by matching
URLs against the forge's address was rejected — it fails in the one case it exists for, after the
forge moves. Credentials for private repositories are left undecided and said so.

Design: new to-be 26 (the seats); 23 gains the seat step in resolution; 18's source entry names
the two forms; the glossary's seat and provision entries say where they meet. 0109 is carried from
its own branch so every link here resolves.
2026-09-25 20:33:14 +02:00
jschoubben 59c93dcfe4 109: a package registry seat is one per ecosystem, not one for all of them
Extends ADR 0075. Surfaced fixing builder's hand-faked package-registry
binding tonight: gitea's manifest declares the provision once with a single
npm-path, conflating what should be independently assignable per ecosystem
(npm/cargo/docker/...) the same way artifact-store and package-registry
were themselves split. Cited in 22-the-work-ahead.md's Phase 2, where the
target state this decision points at was already described a week ago.

Numbered 109, not 108: route-proxy's policy feature (mesh-controller PR
still-unwritten decision record — reserved but never committed. Renumbered
around it rather than colliding.
2026-09-25 20:29:22 +02:00
jschoubben 2ca63ae54e 117: builder's real package-registry grant deadlocks a genesis bootstrap
Fixing builder's hand-faked package-registry binding tonight (requires:
package-registry, a real mesh grant instead of a hardcoded JSON fragment)
broke three tests describing a deliberate carried-binding fallback for
exactly this: gitea's own image is built by builder, so builder cannot
yet hold a real grant from gitea the first time either has to exist.
Invisible on novox (already bootstrapped, gitea already live) — real on
any genesis from scratch. Fix left in place, tests left failing rather
than reverted or hacked, so the gap stays visible.
2026-09-25 16:59:04 +02:00
jochen 82a6badc7c Issue 114: land the controller's container-or-process question, renumbered
Filed 2026-09-24 on a branch of its own and never merged, numbered 113, which is taken. 114 is
free because a sibling branch folded it, so it takes that number and keeps its commit.

Kept separate from issue 117 rather than folded into it. 117 asks the same question of every
module and locates the missing decision; this asks it of the controller, where `network: host`
means container network isolation — the property that resource type usually buys — is not in use.
That observation is this report's own and is nowhere in 117, and folding would lose it.

Its first open question is answered by 117's diagnosis and now says so: the host's `process` shape
is built, applied and tested, restart and run-to-completion semantics included, so deciding this
does not wait on host-side work.
2026-09-25 16:16:21 +02:00
jschoubben 10a2b706c6 Issue 113: should the controller be a container or a process the host supervises
Filed after a session where every mesh-controller interaction went through
docker exec — its manifest runs it as a container with network: host, using
none of the isolation that resource type usually buys, while ADR 0006 makes
it the mesh's single point of coordination. Open question, not a claimed
defect: does type: container get the controller anything type: process
(supervised the way the host supervises its own unit, per ADR 0005) would not.
2026-09-25 16:15:27 +02:00
jochen 35db2aaa41 Issue 117: a module's own code is a container in one record and a process in another
Asked what the "sidecar" is and whether a supervised process would do instead. The repository
answers both ways. ADR 0047 (accepted, unsuperseded) says a module with tools or events runs a
container carrying its compiled code. To-be 18 and 20 (both proposed) define a `process` resource
type — the module's own code, a unit the machine's supervisor keeps up — and the worked guide says
plainly "it is why these are `process` rather than four containers." Neither design doc names 0047,
and no decision record mentions a `process` shape at all.

Diagnosed rather than left open, because the ground truth settles what the report could not.
The shape is real: mesh-host defines TypeProcess, applies it, and tests it, and the host's
vocabulary is twelve shapes rather than the nine ADR 0029 counted. So the alternative the report
offered — that two proposed documents describe a type that does not exist — is disproven.

ADR 0029's mechanism is intact and was not enough. The vocabulary-count test names the decision
behind each addition: network 0029, access 0051, opening 0100. The eleventh names a *proposed
design document*, and TypeProcess is the only shape in the vocabulary whose doc comment cites no
ADR. Requiring every addition to name something does not require it to name a decision.

The argument this issue asked for already exists — as a Go test comment. "It is a full-host shape
rather than a portable one: it needs a process supervisor to install into. It does NOT need a
container runtime, which is the point — only software that genuinely needs isolation asks for a
container." That is a decision's context and consequences, in another repository.

What the catalogue does is a third thing: 115 container declarations against 3 process, all three
in showcase — the module to-be 20 documents. There the tools resource is a container running
`sleep infinity` on a bare upstream base with the broker credential mounted, and the tools and
provisioner entrypoints are run by nothing. That is the condition 0047 was written to end, back
in a new shape.

Where the isolation argument leaks is narrower than expected and worth having precisely: the
serving key and the credential shape both conform. But serveTools serves every registered module
over one broker connection, the runtime takes its modules from a comma-separated list, and
x-source is stamped from the single credential — so two modules in one runtime means the second's
events are attributed to the first. Nothing refuses it and no test asserts against it.

Located on hq rather than on a code repository: the implementation and the design layer agree,
and the missing thing is the record. Which shape is right is left open, deliberately — this
establishes that the question was answered in practice and never written down, not which answer
is correct.

One correction kept in the trail: the first search here was for len(Vocabulary()), found nothing,
and was two steps from being written up as "the mechanism ADR 0029 relied on is gone." The test
binds the slice to a local first. A negative search result read as a fact about the world is the
same error issue 113 recorded.
2026-09-25 16:15:00 +02:00
jschoubben 56669ee23b Merge pull request 'Issue 116: route-proxy has no authentication or IP-restriction mechanism' (#109) from issue/116-route-proxy-has-no-auth-or-ip-restriction into main 2026-09-25 12:28:50 +00:00
jochen 367df38e6d Issue 116: resolved by mesh-controller PR #58
The gap is closed in the proxy: policy applies, the four capabilities exist, the table is keyed
by host and path with a total ordering, and the two failure modes that rot quietly are held by
tests — a declaration carrying a credential refused rather than served, an unreadable secret
failing closed.

Resolved rather than left open because the issue reports a gap in the proxy and that gap is
gone. But the record says plainly what it does not yet allow: an operator still cannot move the
affected routes, because that needs the mesh side — a manifest able to declare these values and
the controller minting the secret auth names. Until both exist the capability is reachable only
by writing the routes file by hand. That is the ordinary build-out of a contract this issue's
decision created, and it belongs to to-be 08 rather than here.

The open questions are marked answered and kept rather than deleted, pointing at ADR 0108 —
what was rejected and why is the half worth having, and a section still saying "the fix should
not be written before these are answered" after the fix was written reads as though nobody
looked.

One finding kept in the record: priority was read with the reader for ports, which caps at
65535, and the one real rule this reproduces is declared at 100000. It parsed to zero, so
refusal and path scoping would have shipped looking complete and doing nothing on the only case
that motivated them. A validator borrowed from a neighbouring field is a silent default.
2026-09-25 14:25:24 +02:00
jochen a11da86591 ADR 0108: a route carries the policy applied to a request
Issue 116 found the mesh's proxy applies nothing to a request — host lookup, forward. Against
what the replaced ingress actually relies on, four capabilities are missing: authentication
(three dependents, each gating an admin surface with no login of its own), refusal scoped to a
path (one, a live incident mitigation), path-scoped routing with priority, and redirect.

Policy goes on the route rather than beside it. A proxy-side settings layer keyed by route name
would keep the grant literally clean, but then "what protects this route" is answered from two
files nothing keeps in step — and a route's protection is part of what a route is.

The set is closed at those four, so a fifth is an amendment and each addition is earned by a
dependent that exists. An open middleware surface was rejected: it recreates what is being
replaced, and narrowing one later is far harder than widening a closed one.

Where policy needs a credential the declaration names a secret and never carries the value,
which keeps the existing secret machinery the only thing holding credentials. Inlining a hash
was rejected as the first credential in a declaration — a precedent easier to set than withdraw.

This re-keys the routing table by host and path with priority, which follows from the decision
rather than being a separate one: two of the four need one host routed more than one way. Equal
priorities must resolve identically every time or the proxy stops being reproducible.

The record says how it is checked, including the negative case that rots quietly — a
declaration carrying a credential value rather than a reference must be refused, so the
rejected option cannot return by accident.

08-connectivity §3 names the record and gains the subsection; issue 116 gains amended-design.
2026-09-25 13:48:18 +02:00
jochen c839d9ac26 Issue 116: scrub the disclosure, and correct the count and the shape of the gap
Two things the report got wrong, and one it could not have found the way it looked.

Disclosure first: it carried a real hostname and an absolute node path, in a public
repository. Both are gone; the ingress, the modules and the routes are named by role, as the
rest of 04-ISSUES does.

The count was low. Basic authentication has three dependents in the catalogue, not one — the
key-value store's browser UI, a database web UI, and the ingress's own dashboard. All three
are credential-less admin surfaces whose only gate is a middleware the mesh's proxy lacks.
The earlier version read only the node's dynamic configuration directory, which cannot see
what modules declare as container labels; counting needs both sources, and the report now
says so.

Two gaps were missing entirely. Redirect rules: two live routes canonicalise a www name onto
its apex, they exist only on the node and not in the catalogue, and they fail silently rather
than erroring. And path-scoped routing with priority, which is the one that reorders the
issue: the table maps host to exactly one target, so a host cannot be routed two ways, and
the refusal rule matches a path on a host already routed elsewhere. Authentication and a
source filter would not make it expressible. Path scoping is a prerequisite, not a sibling.

Also corrected: the refusal rule was described as an address-scoped deny. It is an allow-list
holding a single documentation-range address — deny-everyone — so reading it as address-scoped
points at the wrong fix. And its severity was understated: its own header records it as
incident response closing an abused write primitive, which is not "a real exposure" but a live
mitigation.

The open questions now say plainly that they are design questions and the fix should not be
written before they are answered, and one is added: whether a declaration may carry a
credential at all.
2026-09-25 13:24:34 +02:00
jschoubben 226d556743 Issue 116: route-proxy has no authentication or IP-restriction mechanism
Comparing route-proxy against what HAL's actual Traefik config does today,
not Traefik's general feature set, per the standing rule that the nox mesh
must do at minimum what the HAL mesh it replaces already does. Everything
else checked out even or better; these two are real, confirmed gaps —
RedisInsight has no login of its own and depends entirely on Traefik's
basicauth middleware, and the gitea-internal route depends on an IP-scoped
deny rule. Neither has any equivalent in route-proxy's single-lookup
request path.
2026-09-25 11:51:56 +02:00
jochen ab7d216fc6 Issue 113 resolved by the repin, and what issue 064 did not cover
The object-store module was repinned to a maintained fork of the withdrawn server image, its
runtime sidecar built rather than pulled, and its data moved off the predecessor's live
directory. The instance is closed; the three general points the report makes are not, and What
was done says so rather than letting a resolved status imply otherwise.

Folds in the one thing a duplicate report of this symptom had that this one did not: issue 064
asked whether the build environment can reach a declared vendor image and assumed that, once
declared, it stays fetchable. Withdrawal is the case that assumption does not cover. The
duplicate is not merged — it carried the reading this report's diagnosis retracts.
2026-09-25 00:55:13 +02:00
jschoubben cdcd4da27e Merge pull request 'Research 015: reopen the comparison — the premise for narrowing to one candidate was false' (#105) from storage/015-reopen-the-candidate-comparison into main 2026-09-24 16:47:18 +00:00
jochen 1d524a1fa8 Research 015: rewrite the comparison — wrong axis, and a missing candidate
The previous version ranked candidates on whether they preserved single sign-on to the
object store's console. That is not a requirement: a "user" of the store is normally an
application, so the requirement is per-application keys scoped to buckets — which the mesh
already mints. And the console login it ranked on never worked; the module's own hook comment
records "policy claim missing", a failing login written up as progress.

It also omitted the incumbent's own maintained fork, which changes the question from "which
product replaces it" into two decisions: repoint, or migrate — and if migrating, to which.
Repointing costs an image reference; migrating costs a data copy, two handler rewrites and a
maintenance window. Repointing does not foreclose migrating, which is the argument for taking
it first.

On the corrected requirement Garage ranks first — its per-key-per-bucket model is the
requirement verbatim, its admin API matches how the mesh provisions, and the highest-risk
consumer is first-party documented against it. Its remaining gap (no versioning, no
server-side encryption, partial lifecycle) is unmeasured against the buckets and is the one
thing that could still disqualify it.

Measured and folded in: 230 GiB logical, 82,496 objects, 468 GiB raw at 2.03x, eight drive
directories on one filesystem on one machine. That last fact decides more than any feature —
the erasure coding is not buying independent-drive redundancy, so the redundancy model is
close to irrelevant and only storage overhead remains, which at this volume is a rounding
error against the headroom.

Both errors are recorded at the end of 01 rather than quietly fixed. A configured feature is
not an observed one; and when a dependency dies, "who took it over" precedes "what replaces
it" — searching for alternatives by construction returns things that are not the incumbent.
2026-09-24 18:46:47 +02:00
jochen 999636e2a8 Issue 113: retract the diagnosis table — the original report was right
The diagnosis carried a table headed "claims that could not be substantiated", denying a
module.json, a digest pin, and an all-zeros runtime digest. All three exist. The table is
withdrawn in full and replaced with what is actually true, plus the two claims that remain
genuinely unverified rather than disproven.

The cause: one repository was searched and absence in it was written up as absence. The
catalogue of the mesh being built is a separate repository, not checked out where the search
ran, and all four claims were about that repository. Compounding it, the predecessor's
object-store module and the one being cut over to were treated as one thing — they are
different files in different repositories, one pinning a tag with no sidecar, the other a
digest with two container resources.

Also corrected in the report: located-in named the wrong repository; the "pins a tag" passage
described the predecessor; the open question about pinning by digest is struck, because this
module already does and it made no difference — a deleted digest resolves to nothing either
way. The section on why nothing broke is now scoped explicitly to the predecessor's
machinery.

The lesson kept in the record: "zero occurrences anywhere in the tree" is only as strong as
the tree searched, and a diagnosis must say which tree. A confident rebuttal of a correct
report is worse than no diagnosis — it sends the next person to the wrong place with a
written record behind them.
2026-09-24 18:46:47 +02:00
jochen 48abc36b5b Merge remote-tracking branch 'origin/main' into work/object-store-records 2026-09-24 18:41:53 +02:00
jschoubben 2c5f805467 Merge pull request 'Add hq-defer: park a thought without moving the work off course' (#107) from meta/hq-defer-skill into main 2026-09-24 16:39:22 +00:00
jochen db2c950ba6 hq-defer: make the MEMORY.md pointer an explicit placeholder
Review caught it reading as a real relative link, so a link checker flags
.claude/skills/hq-defer/file.md forever. Angle brackets say placeholder.
2026-09-24 18:38:43 +02:00
jochen 971d0839f5 Add hq-defer: park a thought without moving the work off course
A thought raised mid-task needed remembering but not working on, and there was no
mechanism for that — so it was recorded by hand. This is that, made repeatable.

Records to Claude's persistent memory rather than the repository, deliberately. A parked
thought has no number, owner or status: giving it one asserts triage that deferring says
has not happened. A shared "deferred" document would be a central status file, which
AGENTS.md forbids. And a repository write means a branch, a commit and an MR — the drift
the skill exists to prevent.

Wraps no playbook, because deferring precedes the development cycle rather than being part
of it. It does say which playbook a thought would need if it graduates, and that recording
"undetermined" is the honest answer when the evidence does not say.

The stop condition is the substance: at most two lines, then return to what was in
progress. No plan, no triage question, nothing opened.
2026-09-24 16:40:24 +02:00
jschoubben 5677e97508 Merge pull request 'ADR 0107: persistent data is a directory bind, never a named volume' (#106) from decide/0107-persistent-data-is-a-directory-bind into main 2026-09-24 14:24:17 +00:00
jschoubben a93743708c ADR 0107: persistent data is a directory bind, never a named volume
Records the rule the operator gave directly, mid-session, after checking
that HAL's own postgres and lavinmq both used a directory bind and the
mesh's adoption of them three weeks ago switched to a named volume without
a reason recorded anywhere.

Already built and rolled out on novox (mesh-catalog PR #54) before this
record -- urgent enough to fix first and write down after. Includes the
incident: the new host directories needed the container's own UID, which a
named volume gets for free and a directory bind does not; mesh-store
crash-looped on Permission denied until ownership was matched to what the
original volume already had.

Closes issue 115. Checks pass.
2026-09-24 16:21:51 +02:00
jochen c8cbbcfb8a Research 015: reopen the comparison — the premise for narrowing to one candidate was false
SeaweedFS was scoped as primary because it looked like the only candidate preserving
OIDC console login. Measured: its admin UI is Apache-2.0 but its identity-provider
integration is not — console SSO sits behind the per-TB commercial licence, alongside
point-in-time recovery and automatic EC repair. The free build gives OIDC on the S3 API
via STS and a console authenticated by local username and password.

So the answer to the gating question is that no candidate preserves the current feature
set for free, which this effort had written down as a possible outcome. Reopened across
three candidates with the requirement-by-requirement evidence in 01.

Two corrections to what the overview recorded. RustFS is not a binary-level drop-in
retaining existing data: API and on-disk compatibility are separate paths and the on-disk
one is preview-scoped. And it carries an open defect in the credential path the bucket
provision depends on, which gates it specifically.

Nothing graduates before two measurements named in 01: whether an authenticating proxy
is an acceptable answer to console SSO, and which S3 endpoints consumers actually call —
the latter because Garage does not implement the full span and cannot be ranked until
that is counted.
2026-09-24 16:10:33 +02:00
jschoubben a458f751c6 Merge pull request 'Accept ADR 0038; close issue 091' (#104) from issue/091-ports-are-mesh-assigned-not-manifest-fixed into main 2026-09-24 13:53:36 +00:00
jschoubben 402b798ab6 Accept ADR 0038; close issue 091
The decision (the mesh assigns a container's machine-side port; a module
says only what it needs) was proposed 2026-09-01, and the machinery
already implements it in full -- internal/inventory/ports.go's PortFor,
declaration.go's publishedOn. What was missing was the catalogue actually
complying: 14 of 46 modules baked a machine-side number into their own
manifest anyway. mesh-catalog PR fixes 11 of them (the two defensible
kinds -- foundation, protocol-fixed -- are left alone, per the issue's own
categories). Accepting the decision now that it's actually enforced, and
closing the issue it was blocking.

Checks pass.
2026-09-24 15:52:45 +02:00
jschoubben f10dce4f9e Merge pull request 'Issue 113 and research 015: the object store's images are gone upstream, not access-restricted' (#103) from storage/113-the-object-store-lost-its-upstream into main 2026-09-24 13:45:34 +00:00
jochen 4beb6629db Issue 113: ground the rebuildability point in what the design actually says
Review of my own text found an unattributed claim — "the mesh's claim that a node
can be rebuilt from its declarations" — which is not a stated principle anywhere.
Replaced with the design position that genuinely covers it: to-be 07 chooses
references over payload because "reproducibility comes from pinning the identity of
a thing rather than carrying its bytes". This incident is that choice's failure mode
when the identity stops resolving, which is a sharper point than the one I made.

Scope stated honestly: the passage is about the foundation bundle and this module is
not in it, but pin-identity-fetch-bytes is how every module gets third-party images.

Also names the tension the mirroring question actually carries — mirroring is a move
away from references-over-payload, so it is a decision, not a fix.
2026-09-24 15:44:44 +02:00
jochen d497b37e43 Issue 113 and research 015: the object store's images are gone upstream, not access-restricted
The symptom arrived diagnosed as "the registry disabled anonymous pulls for the
whole vendor namespace". It did not hold: sibling repositories in that namespace
pull normally, the "$disabled" token field appears on every repository including
working ones and describes signing rather than access, and "actions": [] with a
401 is byte-identical to what an invented repository name returns. Both registries'
own APIs establish deletion instead.

Recorded because the correction is the expensive part to rediscover, and because
the instance was harmless while the standing condition is not: no node that does
not already hold the images can ever provision the module again, and nothing
detects that until one tries.

Research 015 scopes the replacement. It is not a redesign — the foundation design
already commits to S3 the protocol rather than the product, and the object store
is an ordinary module, so this instantiates an existing principle. The live OIDC
wiring is the requirement that gates the choice, and it is checked first.
2026-09-24 15:27:03 +02:00
jschoubben 183b22997c Merge pull request 'Issue 112: diagnose — the predecessor's own DNS config already names the carried peers' (#101) from issue/112-diagnosis into main 2026-09-24 12:49:50 +00:00
jschoubben 8d67cf63c5 Issue 112: status located, not diagnosing
Playbook 03 step 2: move status to diagnosing, then located once the
owner is known. located-in is filled with four confirmed packages —
the owner is known.
2026-09-24 14:27:25 +02:00
jschoubben 75c104c355 Issue 112 diagnosis: correct located-in attribution
The carried-peer record (CarriedPeer/TunnelPeer) lives in mesh-controller
internal/inventory, not internal/catalogue. internal/catalogue is the
right package for the zone-generation side of the fix (facts.go's
nodeZones), but a different concern from where the name field itself
would go. Split the two so a decision record doesn't get pointed at the
wrong package.
2026-09-24 13:54:20 +02:00
jschoubben 6abfec7433 Design 25: address first review's four findings before any code
Fixes, each named where it was wrong:

- reload-on is a service field; a container only has restart-on, which
  recreates. Cited precedent (registry-trust-reload) is a service resource,
  not a container. Fix: nats-server's own SIGHUP reload, triggered by an
  in-image entrypoint watching a directory-mounted config file (issue 103's
  recreate-on-change applies to a directly-mounted file, not a directory's
  contents) -- asks nothing new of the host.
- A JetStream-delivered message's Reply field is already claimed by the
  consumer's own ack address, so a responder using it answers nobody. Fix:
  every CONTROL message needing a reply carries its reply subject in its own
  payload; the controller publishes there explicitly, never via Respond().
  Enrolment is the case this design actually depends on, so it's fixed there
  too, not just noted.
- The listed permissions never granted publish on a durable consumer's own
  ack-reply subject -- a module could receive but never ack, so every
  message redelivers forever. Fixed with a scoped grant per module's own
  consumer.
- One account (a deliberate choice, kept) means inbox privacy is the
  permission list or nothing. The design granted 'its reply inbox' without
  scoping it, which read as any user reaching any inbox. Fixed: each user's
  inbox prefix is derived from its own identity and its permissions name
  only that prefix.

New open question from this revision, not closed: whether the in-image
watch-and-SIGHUP shape belongs in mesh-sdk if a second module ever needs it.

Checks pass (records.py, cycle.py, index.py).
2026-09-24 13:53:53 +02:00
jschoubben 6a56738d7b Issue 112: diagnose — the predecessor's own DNS config already names the carried peers
Checked why ADR 0104's forward-to-predecessor shape doesn't transfer to the
resolver the way it did the proxy: DNS is one process on one port, and
assigning the mesh's dnsmasq module replaces it in place, so there is no
predecessor process left standing to forward to.

But /etc/dnsmasq.d/hal-dns.conf's static address= lines for ace/shanks/g14
match the mesh's own carried-peer addresses from overlay show exactly. The
name a carried peer needs isn't a guess the operator has to make under
pressure — it's a transcription of a record the predecessor already has and
has been correctly serving for six days. Located in mesh-controller (no way
to attach a name to a carried peer today) and the dnsmasq module (doesn't
emit a wildcard for a named-but-uncarried peer). Not implemented.
2026-09-24 13:25:44 +02:00
jschoubben 91a5c63d65 Merge pull request 'Name the migration repository in the map, so nobody has to be told it exists' (#99) from meta/name-the-migration-repository into main 2026-09-24 00:02:07 +00:00
jschoubben 545d038198 Name the migration repository in the map, so nobody has to be told it exists
hq cannot hold the migration's operational record — it names machines, addresses and
paths, and this repository is public — but it can say where that record is, which is what
this map is for. Asked for by the operator, who had to be told.
2026-09-24 02:01:41 +02:00
jschoubben 7f438d049f Merge pull request 'Issue 092: genesis publishes to a registry the container runtime does not yet trust' (#79) from issue/092-genesis-registry-trust into main 2026-09-23 23:38:46 +00:00
jschoubben 60e43f9446 Merge pull request 'Issue 091: a module definition carries a machine port' (#78) from issue/091-machine-ports-in-manifests into main 2026-09-23 23:38:40 +00:00
jschoubben 36aa722c2a Merge pull request 'Issues 111 and 112: the resolver was told the wrong set of names, twice over' (#98) from issues/111-112-the-resolver-was-told-the-wrong-names into main 2026-09-23 23:32:42 +00:00
jschoubben f704e2ca64 Issues 111 and 112: the resolver was told the wrong set of names, twice over
111, resolved: the map the control plane hands a resolution holds the machines and the
names the mesh merely serves, and the resolver's zones were given both — inventing names
under a suffix it answers authoritatively for. 112, open: adopting a tunnel gives the mesh
the peers' addresses and none of their names, so taking the resolver before they enrol
stops three machines resolving at all.

Both found by reading the plan before pushing it.
2026-09-24 01:32:04 +02:00
jschoubben e7a90be3ee Merge pull request 'Issue 110: on a converged node a container on the runtime's own network cannot reach the resolver' (#97) from issues/110-the-resolver-and-the-default-network into main 2026-09-23 23:13:48 +00:00
jschoubben 3a8515273d Issue 110: on a converged node a container on the runtime's own network cannot reach the resolver
Found reviewing the resolver's conversion. Nothing fails while the node is adopted; it
fails at the flip, and it is the same split that decided which container survived the
hub's address change.
2026-09-24 01:13:30 +02:00
jschoubben 0e70ca0808 Merge pull request 'Issue 109: a container keeps the address it was made with' (#96) from issues/109-a-container-keeps-the-address-it-was-made-with into main 2026-09-23 23:02:48 +00:00
jschoubben 6ccd138729 Issue 109: a container keeps the address it was made with
Found when adopting the tunnel moved the hub's address: the declaration followed, the
running container did not, and the forge lost its database. Issue 102's rule broken one
level down, and issue 103's fix stopping one input short.
2026-09-24 01:02:16 +02:00
jschoubben 07ee79199a Merge pull request 'ADR 0105: what review settled — the tunnel adoption is implemented' (#95) from decide/0105-implemented into main 2026-09-23 22:39:08 +00:00
jschoubben f660620637 ADR 0105: what review settled — carried peers, the flip, the refusals, and keeping the hub's identity
Implemented in mesh-controller #49 and mesh-host #24. One proposal was rejected on the
record's own terms: converging the hub is not made to wait on other machines' migrations.
2026-09-24 00:38:54 +02:00
jschoubben f61f047a5d Merge pull request 'Issue 102 resolved and verified on the machine; issue 097's orphan was on the host network' (#94) from issues/102-resolved-and-097-worse into main 2026-09-23 22:15:49 +00:00
jschoubben 133e10a738 Issue 102 resolved, verified on the machine with both forwarders gone; 097's orphan was on the host network
The addresses follow: the control plane holds the ports the node gave, a recorded build
holds no address at all, and the two forwarders that had been holding the control plane
together are removed. 097's stranded container turned out to be listening on every
interface and connected to the mesh's store — by its own old database, which is the only
reason nothing was at risk.
2026-09-24 00:02:13 +02:00
jschoubben f3ad60b98c Issue 103 resolved by mesh-host #22 2026-09-23 23:44:31 +02:00
jschoubben 0c433d51ad Design 25: the bus on NATS — subjects, streams, accounts as configuration, enrolment, a person's client, the cutover, the beds
The architecture ADR 0106 asks for, proposed for review before any code.
2026-09-23 23:42:42 +02:00
jschoubben c209a575e1 Merge pull request 'ADR 0106: the bus is NATS; issue 104 resolved' (#92) from decide/0106-the-bus-is-nats into main 2026-09-23 21:40:03 +00:00
jschoubben e022798858 ADR 0106: the bus is NATS — native, built beside the migration, cut over after its core; issue 104 resolved 2026-09-23 23:39:17 +02:00
jschoubben 6f173a7912 Merge pull request 'Issue 108: the registry has no garbage collection, and two doors make it harder to add' (#91) from issues/108-registry-gc into main 2026-09-23 21:32:52 +00:00
jschoubben 73091dcb4c Issue 108: the registry has no garbage collection, and two doors make it harder to add 2026-09-23 23:32:29 +02:00
jschoubben f6ec64ee4e Merge pull request 'Issue 107: a declaration carries no order; rescue on an enrolled node is reconcile, not apply FILE' (#90) from issues/107-declarations-carry-no-order into main 2026-09-23 21:27:24 +00:00
jschoubben 671c2f3881 Issue 107: a declaration carries no order; rescue on an enrolled node is reconcile, not apply FILE
Both from the review of the issue-104 fix: a hand-applied file on an enrolled node is
recorded as carried and would remove the foundation, and nothing on the wire orders one
declaration against another.
2026-09-23 23:27:08 +02:00
jschoubben 2445d80565 Merge pull request 'Research 014: the bus on NATS — decide now, build in the lab, cut over once after the core' (#89) from research/014-nats into main 2026-09-23 21:15:22 +00:00
jschoubben a548b34f5d Research 014: fix the reference to ADR 0039 2026-09-23 23:14:57 +02:00
jschoubben b59907ee08 Research 014: the bus on NATS — decide now, build in the lab, cut over once after the core
Measured: AMQP is spoken in three places of the mesh's own code and in none of the
sdk or the modules; the predecessor's world is AMQP and retiring. NATS answers every
guarantee the bus relies on, durability via JetStream. Recommended: not underneath
the migration, not after it either — in parallel, one rehearsed rollout.
2026-09-23 23:14:39 +02:00
jschoubben 8638ba3a4f Merge pull request 'Issues 102–106 and ADR 0105: what the core migration found, and the hub adopting the predecessor's tunnel' (#88) from core/issues-102-106-and-tunnel-adr into main 2026-09-23 20:54:53 +00:00
jschoubben cb2117f1c4 Issues 102–106 and ADR 0105 from the core migration
Two birth-address outages and a registry that would have been the third; a
container that keeps a stale environment after its file changes; a host command
that applied a converged declaration to an adopted node; the hub and the vault
without seats. And the decision the operator made under it all: the hub adopts
the predecessor's tunnel in place, key and peers and range and port.
2026-09-23 22:50:10 +02:00
jschoubben ee2bdf220c Merge pull request 'Issue 101: taking a service its neighbours reach by container name cuts them off' (#87) from issues/101-a-service-reached-by-name-loses-its-network into main 2026-09-23 18:14:08 +00:00
jschoubben 61e4e971a4 Issue 101: taking a service reached by container name cuts its neighbours off
Found checking the third cutover rather than running it. The first two were safe by
accident — both are reached through a host port, which survives a change of owner.
This is the first constraint found that decides the order of the migration.
2026-09-23 20:13:53 +02:00
jschoubben f4f58e7c30 Merge pull request 'Issue 100: a secret the mesh mints cannot be the one the service it takes over already uses' (#86) from issues/100-a-minted-secret-cannot-be-the-one-the-service-already-uses into main 2026-09-23 00:54:21 +00:00
jschoubben 157c6edd50 Issue 100: a minted secret cannot be the one the service already uses
Found at the second cutover. Carrying a value in works only for a module's own
secrets; a secret answered by the provision is minted, and six catalogue modules
take one that way.
2026-09-23 02:54:09 +02:00
jschoubben e9e5df36bd Merge pull request 'Issue 099: a module's image pin ages into a downgrade, and taking it over is where that is discovered' (#85) from issues/099-a-pin-ages-into-a-downgrade into main 2026-09-23 00:48:02 +00:00
jschoubben 9faa985be3 Issue 099: a module's image pin ages into a downgrade
Three modules in a row on one machine; the first was found by taking it and cost a
three-minute outage. The runbook's answer is a rule a person must remember, which is
the shape this repository says not to settle for.
2026-09-23 02:47:50 +02:00
jschoubben cda7a4e348 Merge pull request 'Issue 094 diagnosed and resolved; 096, 097 and 098 opened from what it uncovered' (#84) from issues/094-diagnosis-and-096 into main 2026-09-23 00:37:27 +00:00
jschoubben 668ce3ad62 Issue 094 resolved: a given port names either end and is answered once
The first pass answered under both ends, which review showed is the same fault seen
from the other side where two mappings share a number. Verified on the machine: the
forge is back on the port its own configuration has always advertised.
2026-09-23 02:37:07 +02:00
jschoubben 0c8615aa8f Issue 098: taking a module replaces a configuration nobody compared
Found reading the second module's cutover rather than running it: the catalogue's
config drops a rule the machine's has, and no step puts the two side by side.
2026-09-23 02:23:58 +02:00
jschoubben 235b9ea0e5 Issues 096 and 097, and 094 diagnosed: a setting stored where it cannot work, and a resource that changed target
094's cause is one blind spot read from two ends, written up in its diagnosis; the fix
answers the first open question and not the other two, which become 096. 097 was found
looking at what the forge's cutover left running.
2026-09-23 02:20:48 +02:00
jschoubben 1b8e5043ee Merge pull request 'Issues 094 and 095, both found in the first module's cutover' (#83) from issues/094-095-from-the-first-cutover into main 2026-09-22 23:57:48 +00:00
jschoubben 515cb8adc1 Issues 094 and 095, both found in the first module's cutover 2026-09-23 01:57:10 +02:00
jschoubben 0d8b683ad7 Merge pull request 'Research 013: the forge and the registries — a seat answers the wrong question' (#82) from research/013-the-forge-and-the-registries into main 2026-09-23 01:03:10 +02:00
jschoubben 43b55d6664 Research 013: the forge and the registries — a seat answers the wrong question; issues 090 and 085 corrected from the code 2026-09-23 00:38:34 +02:00
jschoubben 6c81ea2204 Merge pull request 'ADR 0104: a provision may be answered by an adapter to the predecessor' (#81) from decide/0104-route-adapter into main 2026-09-22 23:57:54 +02:00
jschoubben a23ede495e ADR 0104: a provision may be answered by an adapter to the predecessor; issue 093 located; connectivity says how the proxy hands over 2026-09-22 23:57:38 +02:00
jschoubben a2323ade2e Merge pull request 'Issue 093: the successor proxy cannot serve what the predecessor still serves' (#80) from issue/093-proxy-handover into main 2026-09-22 23:57:04 +02:00
jschoubben d697c6f776 Issue 093: the successor proxy cannot serve what the predecessor still serves, so no web module can migrate one at a time 2026-09-22 23:45:49 +02:00
jschoubben a7b3f7823b Issue 092: it happened twice more — the registry's mesh name, and the mesh's own trust naming the default port 2026-09-22 23:37:05 +02:00
jschoubben cc3af29084 Issue 092: genesis publishes to a registry the container runtime does not yet trust 2026-09-22 22:43:17 +02:00
jschoubben fb9d3035d4 Issue 091: a module definition carries a machine port, measured across the catalogue 2026-09-22 22:26:14 +02:00
jschoubben dd81523eab Merge pull request 'Issue 085 resolved' (#77) from fix/085-resolved into main 2026-09-22 22:00:49 +02:00
jschoubben 5c5821b210 Issue 085 resolved: the packages port is a node setting; two of its open questions stay open 2026-09-22 22:00:38 +02:00
jschoubben c46c508c03 Merge pull request 'Issues 088, 089 and 090, found fixing 085' (#76) from fix/issue-085-followups into main 2026-09-22 22:00:02 +02:00
jschoubben 1728765fe3 Issues 089 and 090: a contributed route does not follow a moved port; the forge module cannot take over the forge genesis raised 2026-09-22 21:54:35 +02:00
jschoubben e609ccdbfb Issue 088: the forge's own address names a port it may not have 2026-09-22 21:41:58 +02:00
jschoubben 60eae92fb1 Merge pull request 'Adoption mode as built: ADRs 0101–0103, issues 084–086' (#75) from feat/adoption-mode into main 2026-09-22 21:02:00 +02:00
jschoubben 7bbbfb158a ADR 0103: a unit is found when an administrator installed it or the machine uses it 2026-09-22 20:02:18 +02:00
jschoubben 87ae893407 Issue 087: the controller cannot tell that a node's host is too old for what it sends 2026-09-22 19:43:01 +02:00
jschoubben c74ea2a4a8 Records say what the build does: 0101 names only measured daemons; 0102 adds to lists and keeps what it writes over; 0103 names every held kind, the found-service rule, conflicting found rules, guards, and what of 0100 it replaces; designs 05, 09 and 17 in step; issue 084's diagnosis in its own file 2026-09-22 19:38:16 +02:00
jschoubben ca1f648973 Issue 086: taking a module narrows a port the predecessor served, without saying so 2026-09-22 19:00:59 +02:00
jschoubben 019184ec0f Issue 085: the packages port given at genesis is not a setting, and a later module can undo it 2026-09-22 18:17:21 +02:00
jschoubben 213ab898d6 ADR 0103: what an adopted node holds and what its guard refuses; the node host and connectivity designs name 0102 and 0103 2026-09-22 17:52:58 +02:00
jschoubben 84761f0600 ADR 0102: the mesh writes into a shared file, never over it; issue 084 located 2026-09-22 17:46:06 +02:00
jschoubben 1901a90d68 ADR 0101 accepted; raising a mesh names it 2026-09-22 17:43:13 +02:00
jschoubben 347bbce633 ADR 0101 proposed: a machine's own resolver does not make it in use, as measured on a fresh machine 2026-09-22 17:38:44 +02:00
jschoubben 6165a7ae02 Issue 084: taking networking on an adopted node restarts every container, and the held runtime file blocks pulling 2026-09-22 17:09:21 +02:00
jschoubben dfadfd23c0 Merge pull request 'ADR 0100 (proposed): a node in use is adopted before it is converged' (#74) from feat/adoption-mode into main 2026-09-22 16:35:32 +02:00
jschoubben 3d4ab23830 ADR 0100: the machine's own traffic is known by its interface, not its source address 2026-09-22 16:35:25 +02:00
jschoubben 37252f9c3e ADR 0100: the guard lets the machine itself through; in use is a non-loopback listener; openings say from where; 09 in step with the flip 2026-09-22 16:34:53 +02:00
jschoubben f3152d827f ADR 0100 after re-review: the bus and registry stay reachable for enrolment; the mesh guards the store in a table that only refuses; a machine in use defined; the flip refuses while a found container is held; held containers and returning to adopted spelled out 2026-09-22 16:32:37 +02:00
jschoubben 02c40bcab4 ADR 0100 after review: found means unrecorded; assigning prepares, taking cuts over; openings through the found firewall on both paths; the mesh guards its own ports; ports kept as node settings; a converged genesis refuses a machine in use; designs 05, 07, 08, 09 and 17 in step 2026-09-22 16:28:31 +02:00
jschoubben 111456abb5 ADR 0100 accepted; the node host, connectivity, the node lifecycle and raising a mesh amended for a node adopted before it is converged 2026-09-22 16:20:27 +02:00
jschoubben 5fc3cbde4c Research 012: migrating a node that is in use, measured on the control-node; ADR 0100 proposed — a node in use is adopted before it is converged 2026-09-22 16:16:28 +02:00
jschoubben 21baf397a8 Merge pull request 'Issue 083 resolved: nothing the control queue carries is lost while the store restarts' (#73) from multiple-fixes into main 2026-09-22 14:43:00 +02:00
jschoubben 4de74880cb Issue 083: the proof, the replay caveat, and what is not closed, as three reviews found them 2026-09-22 14:39:29 +02:00
jschoubben 266ee34b5c Issue 083: the diagnosis describes held messages, the enrolment's order, and what is not closed 2026-09-22 14:24:31 +02:00
jschoubben 2d45aa5f42 Issue 083 resolved: nothing the control queue carries is lost while the store restarts; an enrolment claims its token and spends it last 2026-09-22 14:11:44 +02:00
jschoubben 4e74cf29e4 Merge pull request 'Issues 081 and 082 resolved; 083 opened' (#72) from multiple-fixes into main 2026-09-22 13:53:16 +02:00
jschoubben 0d40918e92 Issue 081: proven by the two-node bed, and what it found about baserow's data directory 2026-09-22 13:53:00 +02:00
jschoubben f6ded3102d Issue 083 opened (other control messages lost while the store restarts); 082's diagnosis carries its review 2026-09-22 13:34:05 +02:00
jschoubben 9a75f2b6a9 Issue 082: a report that arrives while the store restarts was lost; 081's diagnosis corrected on review 2026-09-22 13:25:33 +02:00
jschoubben 5d1a0372cb Issue 081 resolved: neither cache consumer can keep its keys under its login, so neither takes the shared cache 2026-09-22 12:27:12 +02:00
jschoubben f1e3925009 Merge pull request 'Issues 079 and 080, found by running the large mesh bed; 074's addendum' (#71) from multiple-fixes into main 2026-09-22 02:19:17 +02:00
jschoubben 35e44c3e5e Issue 081 opened (a cache consumer does not use its login); 079 and 080 diagnoses carry the review's consequences 2026-09-22 02:04:29 +02:00
jschoubben f760bf632c Issue 080: a cache grant let the consumer flush the server; 079: the names follow the resolver's rule for the private network 2026-09-22 01:39:29 +02:00
jschoubben e31e077fc8 Issue 079: the suffix is handed down, not written twice 2026-09-22 01:27:53 +02:00
jschoubben 17cc36069f Issue 079: every machine named twice over, found by the large mesh bed; what running that bed cost, on 074 2026-09-22 01:13:29 +02:00
jschoubben 446553dd1f Merge pull request 'ADR 0099; issues 077, 078 and 074 resolved; designs 08 and 20 amended' (#70) from multiple-fixes into main 2026-09-21 23:58:50 +02:00
jschoubben 3ea5e47c21 Review: 077 says what closed it and what did not; 078 names module issue; 074's retired fixture; ADR 0099's scope 2026-09-21 23:58:15 +02:00
jschoubben ead8913a74 Issue 078: module issue's orphan account, refused before it is made 2026-09-21 23:46:51 +02:00
jschoubben faa7196ad6 Issue 074: opened date restored 2026-09-21 23:44:22 +02:00
jschoubben 8d159cee84 Issue 074 resolved: the declared list is empty of WEARING; what the last three cost 2026-09-21 23:43:31 +02:00
jschoubben 0e0f0298c6 ADR 0099: a step that runs once names what it reads; issues 077 and 078 resolved; designs 08 and 20 amended 2026-09-21 23:33:47 +02:00
jschoubben c2a81cbb20 Merge pull request 'ADR 0098; issue 076 opened and resolved; ADR 0097's base refusal live; ADR 0096 proven against the public hub; 074 down to one bed' (#69) from multiple-fixes into main 2026-09-21 22:58:26 +02:00
jschoubben 6bc9df4b49 Review corrections: 076 and ADR 0098 say what the authority could and could not do; issues 077 (a fetched fact is fetched once) and 078 (secret accept takes any name) opened 2026-09-21 22:55:11 +02:00
jschoubben 6d3cb60949 Issue 076: the route-forwarding bed proves ADR 0098; what the run taught about the overlay 2026-09-21 22:44:24 +02:00
jschoubben 75af72ca27 ADR 0096: the copy is proven against the public hub by the genesis bed 2026-09-21 22:32:03 +02:00
jschoubben 252c6042e8 ADR 0098: a fact a provider makes at first start is fetched from it; issue 076 resolved; design 08 amended; 074 down to one bed 2026-09-21 22:27:32 +02:00
jschoubben bc373797c7 Issue 076 opened: a served fact made at first start cannot be served; ADR 0097's refusal of an undeclared base is live 2026-09-21 22:16:15 +02:00
jschoubben 559683318b Merge pull request 'Multiple fixes: issues 064, 066 and 020 resolved; 074 narrowed to two beds' (#68) from multiple-fixes into main 2026-09-21 22:13:20 +02:00
jschoubben ac11bea77f Issue 020 resolved: the symptom was the bed's, proven against Pebble and step-ca alike 2026-09-21 22:12:51 +02:00
jschoubben c1a10dc3f8 Issue 066 resolved: a file and its reader are guarded by a gate, proven by the coupled-pair spike; design 20 says so 2026-09-21 22:04:14 +02:00
jschoubben e993233004 Issue 074: six of the ten beds retired rather than converted 2026-09-21 21:53:11 +02:00
jschoubben 2d77911511 Issue 064 resolved: the package half placed by the genesis run, the image half by ADR 0097 2026-09-21 21:52:32 +02:00
jschoubben f11824d299 Merge pull request 'Multiple fixes: ADRs 0094–0097; issues 069, 049, 046 resolved; 064's image half decided' (#67) from multiple-fixes into main 2026-09-21 21:05:13 +02:00
jschoubben 43ca9ce0ae ADR 0097: an undeclared base is said, not yet refused 2026-09-21 20:48:19 +02:00
jschoubben 71072e240d ADR 0097: a vendor image is a declared build input; issue 064's image half decided; design 18 amended 2026-09-21 20:45:36 +02:00
jschoubben 8d981e21b1 ADR 0096: an upstream image is copied between registries; issue 046 resolved; design 18 amended 2026-09-21 20:43:07 +02:00
jschoubben 39a01b7f7e ADR 0095: the control plane is the way to ask a module; issue 049 resolved; design 19 amended 2026-09-21 20:38:23 +02:00
jschoubben acc9824949 ADR 0094: a module may hold several secrets from one provider; issue 069 resolved; design 24 amended 2026-09-21 20:29:29 +02:00
jschoubben a259d1292f Merge pull request 'ADR 0093: a fixture that runs a module's runtime carries its name; 074 diagnosed; 075 resolved' (#66) from feat/mesh-tests-and-runtimes into main 2026-09-21 19:36:44 +02:00
jschoubben 76cd91413e ADR 0093: a fixture that runs a module's runtime carries its name; 074 diagnosed, two beds converted; 075 resolved 2026-09-21 19:30:35 +02:00
jschoubben 6427afe6ae Merge pull request 'Issues 072 and 073 name their merges, the branches being gone' (#65) from chore/merges-named into main 2026-09-21 19:25:06 +02:00
jschoubben d92068be7b Issues 072 and 073 name their merges, the branches being gone 2026-09-21 19:24:28 +02:00
jschoubben 6a35e5a58c Merge pull request 'Multiple fixes: status vocabulary aligned, 065/026/070/007 resolved, 066/069/049/046 located, 064 diagnosed' (#64) from feat/multiple-fixes into main 2026-09-21 19:23:34 +02:00
jschoubben bf4d99843f Merge pull request 'Issue 072 diagnosed: genesis registers the manifest its build produced; design 17 says so' (#63) from feat/one-controller-manifest into main 2026-09-21 19:23:31 +02:00
jschoubben 702289ad7d Merge pull request 'ADR 0089: a bed reads the catalogue it proves; issue 073 diagnosed; issues 074 and 075 opened' (#62) from feat/beds-read-the-catalogue into main 2026-09-21 19:23:16 +02:00
jschoubben 6ee95a3f31 ADR 0090: a failure is the same by resource id, not by the host's words 2026-09-21 19:23:04 +02:00
jschoubben 159a583cf0 Issue 069: the count is the catalogue's, not the report's 2026-09-21 17:51:40 +02:00
jschoubben 62ff9a5bd3 Issues 046, 049 and 069 located, each with the decision its fix needs named 2026-09-21 17:50:42 +02:00
jschoubben 16442ecd23 ADRs 0091 and 0092; issues 026 and 070 resolved; designs 18 and 24 amended 2026-09-21 17:50:41 +02:00
jschoubben ffb7fa52ec Issues 007 resolved, 066 located, 064 diagnosed 2026-09-21 17:48:38 +02:00
jschoubben fcf34727fe ADR 0090: a failure that repeats is said to be stuck; issue 065 resolved
The controller kept one report per machine, replaced, so a resource nothing can
ever apply looked like a failure that had just happened, every few minutes, for
ever. It now counts identical reports and status says stuck after three.
2026-09-21 17:43:02 +02:00
jschoubben a5e351f4cb Nine resolved issues name where their fix landed
The cycle check now asks a resolved issue for its owner, and these had none.
2026-09-21 17:39:34 +02:00
jschoubben e3279f4b5b Issue 054 names where its fix landed 2026-09-21 17:39:09 +02:00
jschoubben d52687d0c8 Issues 067 and 068 name where their fix landed 2026-09-21 17:38:48 +02:00
jschoubben 36d9b38a0d An issue is open, diagnosing, located, resolved or wontfix — nothing else
The playbook, the README and the status skill knew five statuses; the cycle check
knew a sixth, 'fixed', and not 'wontfix'. Eleven issues sat in the sixth for weeks
with their fixes shipped, one step short of closed. They are resolved; the check
refuses the word from now on and accepts the one the playbook allows.
2026-09-21 17:38:17 +02:00
jschoubben da6414c27c Issue 072 diagnosed: genesis registers the manifest its build produced; design 17 says so
ADR 0069 had already placed the controller's manifest in its own repository, and the
raising design called the catalogue copy a thing to remove. The installer's step 3 was
handed that manifest by the build and step 9 read a second copy anyway.
2026-09-21 15:18:32 +02:00
jschoubben 5dcb9dfbf6 ADR 0089: a bed reads the catalogue it proves; issue 073 diagnosed; issues 074 and 075 opened
The end-to-end design held 'the run rebuilds what it tests' for binaries and images
and not for manifests. The beds' inline copies fell into three kinds; only the first
is a stale copy. The other two are named: a mesh test wearing a catalogue module's
name (074) and a stocked runtime image the run never rebuilds (075).
2026-09-21 14:35:08 +02:00
jschoubben 1c90777124 Merge pull request 'Issues 031, 035 and 054 name their merges, the branch being gone' (#61) from chore/migration-blockers-merges into main 2026-09-21 13:50:06 +02:00
jschoubben ba07b43b18 Issues 031, 035 and 054 name their merges, the branch being gone 2026-09-21 13:49:56 +02:00
jschoubben 1db93ffc64 Merge pull request 'ADRs 0087 and 0088; issues 031, 035 and 054 resolved; issue 041 surveyed; issue 073 opened' (#60) from feat/migration-blockers into main 2026-09-21 13:49:09 +02:00
jschoubben ea42b00d0a Issue 041: mongodb names its secrets' owner; the three conversions are proven 2026-09-21 13:47:39 +02:00
jschoubben 198e4db0a3 Design 07: the base ruleset closes the hub's port until the filter module derives one 2026-09-21 13:30:26 +02:00
jschoubben a438cbdac3 Issue 041: the declared exceptions, surveyed and partly converted 2026-09-21 12:53:14 +02:00
jschoubben 5fa35362ac Issue 073: beds carry copies of catalogue manifests 2026-09-21 12:31:04 +02:00
jschoubben ee67f69ef4 ADRs 0087 and 0088; issues 031, 035 and 054 resolved; designs 05, 07 and 18 amended
A seeded file is created once (0087); the foundation filters before anything
listens (0088); a machine becomes the last thing it was told (design 05).
Each says how it is checked.
2026-09-21 12:11:50 +02:00
jschoubben a211aecdfa Merge pull request 'ADR 0086 (a secret reaches a process as a file), the vault shipped, issues 041 and 072' (#59) from feat/secret-not-in-environment into main 2026-09-21 11:59:21 +02:00
jschoubben 5fb8f06a91 Issue 072: the controller's manifest exists twice; decisions index regenerated for ADR 0086 2026-09-21 10:34:17 +02:00
jschoubben ec7b6e0748 ADR 0086: a secret reaches a process as a file, and an exception is declared
Closes issue 041 by decision and by code on the same branch: the catalogue
engine refuses a secret in a container's env, and a secret-carrying env-file
unless the container declares its reason; the controller reads all six of
its credentials from files; design 13 states the rule and how it is checked.
2026-09-21 10:10:33 +02:00
jschoubben 7015bf58ac The vault shipped: as-is 06 describes it, design 24 is implemented, 071 names its merges 2026-09-21 10:10:33 +02:00
jschoubben aa9b5b99e4 Merge pull request 'The secrets vault: handoff, the amended decision, issues 069–071, and the reconciliation of open issues' (#58) from feat/secrets-vault into main 2026-09-21 10:03:28 +02:00
jschoubben 61f61b5d06 Reconcile the open issues against main
An audit of the six code repositories found eleven open issues fixed on main
with commits and beds to show (025, 027, 033, 036, 037, 040, 045, 047, 050,
052, 053), three partly (007, 026, 035), nine not (020, 031, 041, 046, 049,
054, 064, 065, 066) and one whose fix would live outside those repos (006).
Resolved ones name their evidence; partly ones say what remains; 041 records
that the exposure has widened since it was reported.
2026-09-21 01:52:53 +02:00
jschoubben ffd7592687 Design 24: the export says what an earlier key opens 2026-09-21 01:16:54 +02:00
jschoubben 1bce3f33a8 Designs 07 and 21 and issue 071: genesis now makes the root secrets
The installer half of the amended ADR 0085 is built and proven by the
genesis bed's root-secrets step; the foundation design closes its open item
and the installation design says what the installer does and what it still
cannot check.
2026-09-21 00:50:56 +02:00
jschoubben 8607d21110 Design 24: pair credentials are sealed to the operator too 2026-09-21 00:36:30 +02:00
jschoubben 050a2d08e8 Playbook 07: a feature worktree needs the siblings the lab reads
A bed run from .work/<slug>/mesh-lab derives mesh-tools and mesh-sdk by
sibling path and fails at once when the directory holds only the touched
repos. Detached worktrees on main, never symlinks.
2026-09-21 00:27:01 +02:00
jschoubben baa3351552 Amend ADR 0085: the vault is a foundation module and holds the root secrets
Recorded on the record, dated, before anything shipped against the sentences
that change. The vault is installed at genesis like the store and broker, one
per mesh, and holds every secret a module has for itself sealed a second time
to an operator key whose private half never enters the mesh — the break-glass
path the first version left open, without a key one place holds.

Design 24 says how; 07 and 21 say what genesis does not yet do; issue 071
names the fixed credentials the foundation is raised with today.
2026-09-20 23:55:01 +02:00
jschoubben 187389b7d1 Hand off the secrets vault to mesh-catalog; open issues 069 and 070
Design 24 flips to in-progress with mesh-catalog as its owner (playbook 04).
Starting the build surfaced two gaps the decision did not settle: a module
requiring `secret` receives exactly one value (069), and no command can
accept an operator's value into a consumer↔vault pair (070). Both opened as
issues rather than improvised around.

Also fills fixed-by on 067 and 068, which the cycle check refused as resolved
with no reference.
2026-09-20 23:08:28 +02:00
jschoubben 731027c009 Merge pull request 'Graduate issues 067 and 068 — provider scoping and the secrets vault' (#57) from multi-node/harden-and-prove into main 2026-09-20 21:30:12 +02:00
jschoubben fc4ab370d6 Graduate issues 067 and 068 to decisions and to-be designs
ADR 0084 (extends 0027) — a provision is served by a node-scoped provider the
consumer selects, defaulting to co-location; a module may instead carry a private
embedded instance that is not a provision. Design: 01-to-be/23-choosing-a-provider.

ADR 0085 (extends 0031) — a secret is a provision and the vault is the module that
provides it; a module's own local secret becomes an ordinary pair credential that
rotates through the existing machinery, while the controller's provisioning-credential
mint (0048) is unchanged. Design: 01-to-be/24-the-secrets-vault; doc 13 amended to
cross-link the non-pair secret.

Issues 067/068 marked resolved with amended-design set. ADR index regenerated;
records and index checks pass.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:27:29 +02:00
jschoubben d66579c8f6 Issue 068 — secrets have no owning module
Store and broker seats were made ordinary modules; secret-minting is still a
privileged property of the controller that no module owns. Propose the vault
become a module that provides a secret provision (generate/hold/rotate/backup/
audit), node-scoped like every other provider (issue 067), subsuming the three
secret paths and giving local-secret rotation a home.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:24:42 +02:00
jschoubben c4ff0478b7 Issue 067 — fold in the embedded-vs-provisioned axis
Naming which provider is only half of how a module gets a database. The other
half: a module may carry its own version/fork-pinned instance, module-network
only, no published port, not a provision — and the model has no word for it.
Add it as a second axis with its own open questions.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:24:42 +02:00
jschoubben 1ce9ffe79f Issue 067 — a provision cannot name which provider serves it
The mesh models provisions as mesh-scoped (one provider of a kind, a single
mesh-store). But node-specific services delivered to the mesh was the plan from
the start: both nodes already run their own postgres, SQL server, redis and
object store, and identity — currently single — already serves apps on a second
node. The model cannot express which provider serves a consumer, so it collapses
a deliberately per-node fleet to one. Provider scoping is a whole-mesh decision
across postgres/s3-bucket/oidc, not an SSO patch.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:24:42 +02:00
jschoubben df2c3f63a5 Merge pull request 'Issues 065 and 066 — the apply failure model's two gaps' (#56) from issue/065-066-apply-failure-model into main 2026-09-20 14:28:18 +02:00
jschoubben a9bc0c1cc5 Open issues 065 and 066 — the apply failure model's two gaps
Surfaced while reviewing the host apply loop for the controller/host
split discussion. 065: a permanently-failing resource retries for ever
with no escalation — reported, but never raises its hand as stuck.
066: a partly-applied declaration leaves a mixed state with no rollback,
which is harmless for independent resources and unexamined for pairs
that are only correct together. Both are questions HQ must answer, not
incidents — status open, no fix proposed.
2026-09-20 14:28:04 +02:00
jschoubben 6d28683898 Merge pull request '057 and 058 resolved — fixes merged and proven' (#55) from issue/057-058-resolve into main 2026-09-20 12:55:16 +02:00
jschoubben 82c03a8d10 057 and 058 resolved — their fixes merged and proven
The P1-sweep PR left both at 'located'; the fixes have since merged
(mesh-controller #31, mesh-tools #10) and the bed proves them. Flip to
resolved with their fixed-by.
2026-09-20 12:55:00 +02:00
jschoubben 31c5aee72d Merge pull request 'ADR 0083 and issues 057/058/060/063/064 — the P1 sweep' (#54) from issue/057-058-one-push-patient-runtime into main 2026-09-20 12:53:24 +02:00
jschoubben 05857f71f0 Accept ADR 0083 — one push leaves the mesh consistent 2026-09-20 12:52:55 +02:00
jschoubben 98e9e62fce Issues 060 resolved, 064 opened; 057/058 on this branch too
060: 44 of 70 modules now mesh-buildable (was 8) — the mechanical
majority, proven by direct builds and the bed. The structural
remainders: external-dependency fetch (new issue 064) and route-proxy's
cross-repo build context. 064 records the build environment's isolation
from public npm and Docker Hub.
2026-09-18 02:52:36 +02:00
jschoubben 0d5693cc2c ADR 0083: the cascade compares against what was last sent, not a snapshot 2026-09-18 02:38:00 +02:00
jschoubben 1572d74a18 Issue 063 — the foundation's ports are forwarded, resolved
The broker's port was accepted on input but not forwarded, so a joined
node reached a DNAT'd broker only until it restarted. Fixed in
mesh-controller (25e42b3), proven by the built-store-cross-node bed
adopting the foundation's broker (which restarts it) and the joined
node still receiving declarations.
2026-09-18 02:20:11 +02:00
jschoubben 9fd7e6c458 ADR 0083 proposed; 057/058 diagnosed and located
One push leaves the mesh consistent (the 057 decision, proposed for
acceptance); the shared runtime waits for its broker (058). Fixes on
mesh-control fix/one-push-is-enough and mesh-tools
fix/the-runtime-waits-for-its-broker; the built-store-cross-node bed
enforces both.
2026-09-18 02:10:01 +02:00
jschoubben 5703c86cf6 Merge pull request 'Issue 059 resolved — the broker credential resolves the seat' (#53) from issue/059-broker-address-seat into main 2026-09-18 01:13:13 +02:00
jschoubben d85d74d607 059 resolved — the broker credential resolves the seat
Fixed by mesh-controller PR 30 (79a9e17), rebased onto the merged
registry-reach train and proven by a green fresh run of the no-fake
two-node bed built from that commit.
2026-09-18 01:13:03 +02:00
jschoubben 46efb7ae98 Merge pull request 'ADR 0082 and issues 042/048/061/062 — the registry is reached by name and trusted by the overlay' (#52) from issue/042-048-registry-reach into main 2026-09-18 01:00:55 +02:00
jschoubben 4279e4914b 042/048/061/062 resolved — the network carries the registry trust
One green fresh run of the no-fake two-node bed is the proof: node2's
consumers open the store and broker the mesh built and adopted, over
the overlay. Fixed by mesh-controller PR 29, mesh-catalog PR 26, gated
by mesh-lab PR 34.
2026-09-18 00:26:50 +02:00
jschoubben a0acaad86d Issues 061/062 — two silent-success defects the no-fake bed surfaced
061: the broker module's provisioner never ran; its runtime container
named no command and the image default is the tool host. 062: a failed
artifact-store lookup composed the network without the registry trust,
turning a transient error into permanent silent state. Both located,
fixes on the 042/048 train branches.
2026-09-18 00:23:52 +02:00
jschoubben 08c814aa0a 0082 takes its homes: 042/048 located against it, the delivery design cites it
https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 23:02:32 +02:00
jschoubben 32fa6b40f5 ADR 0082: the registry is reached by name, and the overlay is its security
https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 23:02:18 +02:00
jschoubben 8df3469813 042 and 048 move to diagnosing — the registry-reach work begins
https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 22:44:39 +02:00
jschoubben e7869d27bd Merge pull request 'Issue 060 — the mesh cannot build most of its own catalogue' (#51) from issue/060-catalog-not-mesh-buildable into main 2026-09-17 22:39:01 +02:00
jschoubben 0b002ef39c Issue 060 — the mesh cannot build most of its own catalogue
Only 8 of 70 modules carry a build section; the other 62 exist through an out-of-band
build script plus placeholder rewriting, so the mesh's own build-and-deliver path has
never run for them. Found by the no-fake bed; blocks it and the migration's delivery
assumption.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 22:38:48 +02:00
jschoubben 101848242f Merge pull request 'Issue 059 — the broker credential names the hub, not the broker's node' (#50) from issue/059-broker-address-review into main 2026-09-17 22:37:26 +02:00
jschoubben 5395b6a47f Merge pull request 'ADR 0081: decisions are in the chain — no orphan records' (#49) from process/decisions-in-the-chain into main 2026-09-17 22:36:48 +02:00
jschoubben 90e4a368dc ADR 0081: a decision nothing cites is not yet in the chain
Decisions were the one link the cycle checks skipped, and measuring found 19 of 70 records
orphaned — the credential flow and the module-runtime cluster among them, which is how a
stale premise about a settled decision survived in working memory. cycle.py now refuses an
accepted record nothing cites; the 19 got true homes (design frontmatter, the playbook that
implements 0021, META for the process records). The overview names the practice: spec-driven
development with provenance.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 22:36:33 +02:00
jschoubben d1b5210d83 Issue 059 — the broker credential names the hub, not the broker's node
Findings of an adversarial review of the 055 fix: hub!=broker conflated, membership tested
as has-address, silent staleness, portless silent fallback, two hubs unrefused. One review
claim recorded as disputed against live wire measurements.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 22:29:25 +02:00
jschoubben 1162f7fe25 Merge pull request 'ADR 0080: the development cycle is checked, not trusted — plus review fixes' (#48) from process/the-development-cycle into main 2026-09-17 22:28:22 +02:00
jschoubben becae7ba51 ADR 0080: the development cycle is checked, not trusted
The flow the process overview draws — idea/symptom -> decision -> to-be design -> code ->
as-is — was enforced by nothing. cycle.py now refuses a to-be design naming no decision, an
in-progress/implemented design naming no owning code, a located/fixed issue with no owner,
a fixed/resolved issue with no fix, and a graduated research overview that does not say what
it became. AGENTS.md carries the cycle and a where-to-look table so a fresh session (or a
cleared context) finds the chain in frontmatter instead of assuming it. Grounding the check
surfaced two real gaps, fixed here: the work-ahead design named no owning code, and research
003 listed one became target twice.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 22:28:03 +02:00
jschoubben 94fee5d849 Fix what the records review found
The glossary's authority page still named the controller's seat the-controller in two
entries, contradicting its own seat section after ADR 0079; issue 058's heading kept the
pre-renumber 059; 055's fixed-by named branches that stop existing after merge (now merge
commits/PRs) and its located-in listed file paths where the convention wants repos; 056's
located-in named mesh-host, which received no fix, instead of mesh-catalog; and the design
layer never said the one-store/one-broker property is enforced — 07-the-foundation and the
installation table now state the seats.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 22:24:57 +02:00
jschoubben 5856555390 Merge pull request 'Issue 058 — a provisioner runtime crash-loops until the overlay is up' (#47) from issue/059-provisioner-startup-resilience into main 2026-09-17 21:57:03 +02:00
jschoubben 0ffdf4e581 The issue takes the next free number, 058
https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 21:56:49 +02:00
jschoubben dd5bf9cd8c Issue 059 — a provisioner runtime crash-loops until the overlay is up
Observed in the two-node bed: a cross-node runtime exits on 'timed out fetching the
broker's certificate' until the tunnel forms, then settles. The retry belongs in-process.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 21:56:40 +02:00
jschoubben 0e9d4ab86a Merge pull request 'ADR 0079: foundation seats are named after their servers; issue 056 resolved' (#46) from multi-node/foundation-seats into main 2026-09-17 02:16:11 +02:00
jschoubben ee7abe0a8e ADR 0079: foundation seats are named after their servers; issue 056 resolved
The store and broker were "one per mesh" by convention only. Each foundation module now
claims a mesh-scoped seat named after the server it guards — postgres/mesh-store,
lavinmq/mesh-broker — and the controller's seat is renamed the-controller -> mesh-controller
so all three follow one rule. The resolver refuses a second holder, closing 056. Glossary,
the foundation and installation docs, and the decisions index follow the new name.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 02:15:08 +02:00
jschoubben f70973e222 Merge pull request 'Issue 055 resolved and proven; 056 and 057 opened' (#45) from multi-node/harden-and-prove into main 2026-09-17 01:44:59 +02:00
jschoubben 812dc3b303 Issue 055 resolved, and 057 opened for its silent operational edge
055 is proven end-to-end by the two-node bed: a consumer on a joined node reaches the
adopted broker over the overlay, its binding names the control-node, and its vhost is
minted. Three fixes — the broker's amqps port in the firewall, the broker credential
naming the overlay not the public address, and pushing the provider node after the remote
consumer arrives. The last is an operator ordering, not a code fix, and its silent-failure
edge (a cross-node consumer that never provisions and only crash-loops) is opened as 057.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 01:35:47 +02:00
jschoubben 33fc322f4d Issue 055 diagnosed — the module broker URL uses the public address, not the overlay
A two-node bed pins it: the firewall gap (broker's 5671 not in listens) is fixed
in mesh-catalog; the real bug is modules.go:294/build.go:231 building the broker
URL with the genesis public MESH_BROKER_ADDRESS, which a joined node cannot reach.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 01:02:07 +02:00
jschoubben bec1dd0082 Issue 056 — an adopted module assigned to a second node raises a second server
"One postgres, one lavinmq" (ADR 0078) holds only by genesis assigning the
adopted modules to the control-node alone; nothing enforces it. A second assign
raises a divergent second server, silently. Open questions: a mesh-scoped
exclusive seat, or explicit adoption the controller refuses to place elsewhere.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 00:22:18 +02:00
jschoubben a32f7e50af Merge pull request 'Establish the repo for the completed Phase 0-3 build' (#44) from establish/phase-3-close into main 2026-09-17 00:05:20 +02:00
jschoubben 1111bd84d7 Establish the repo for the completed Phase 0-3 build
Settles the design repository now that the self-upgrade build is on main:
- Records the two decisions that shipped without a record — ADR 0077 (the
  controller/foundation/node vocabulary) and ADR 0078 (the store and broker are
  ordinary modules); accepts ADR 0075 and 0076, which shipped work rests on.
- Fills issue 051's amended-design and wires ADR 0078 into 07-the-foundation.
- Sweeps the repo rename (mesh-control -> mesh-controller) into the mutable docs
  now that the forge repo is renamed; updates the glossary note and repos.md.
- Fixes the six broken links from the design-doc renames, indexes the glossary,
  regenerates the decisions reading order.

Both checks (records.py, index.py) are green. Statuses stay honest: the build is
on main and lab-proven but not deployed as the production mesh, so the to-be docs
remain in-progress and the as-is layer (the hal mesh) is unchanged — graduation
to implemented + as-is belongs to deployment, not merge.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 00:04:58 +02:00
jschoubben 873cc4007b Merge pull request 'Glossary, the mesh-controller/foundation vocabulary, ADR 0076, Phase 3 closed' (#43) from issue/047-the-other-half into main 2026-09-16 23:25:51 +02:00
jschoubben 0073e52881 Phase 3 closed — the store and broker are ordinary modules, issue 051 fixed
Marks WBS 3.2/3.3/3.4 done and resolves issue 051: the foundation's store and
broker are adopted in place as the postgres and lavinmq modules, upgradeable
through their stated windows, source-tracked by status. A bare-metal mesh runs
one postgres and one lavinmq, proven 22/22 in the one-node lab. The two follow-up
gaps are tracked as issues 054 and 055.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 23:07:03 +02:00
jschoubben fcf3b6f4d5 Issues 054, 055 — the debt adopting the store and broker leaves
054: the adopted servers bind 0.0.0.0 from genesis but the packet filter is
installed later, so there is a window where they are open with only bootstrap
credentials. 055: the servers bind on the control-node and the one-node bed
cannot prove a consumer on another machine can reach them over the overlay.

Both are follow-ups to issue 051's adoption (WBS Phase 3), tracked rather than
rushed.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 22:04:28 +02:00
jschoubben 43f3565617 WBS: correct 3.1 phrasing — a module gets a database only if it asks
Not "every module's database"; modules request one via requires postgres-database.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 21:20:00 +02:00
jschoubben addfdd6940 WBS: Phase 3.1 done — the store is adopted as the postgres module
One postgres, adopted in place, proven 22/22 in the one-node lab. Notes the
pre-filter exposure window as a follow-up.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 21:14:13 +02:00
jschoubben 33a00d5656 Adopt the glossary's vocabulary in the mutable design docs
"control plane" -> controller and "substrate" -> foundation throughout
03-DESIGN, 00-META and the README, with 06-the-control-plane.md and
07-the-substrate.md renamed to 06-the-controller.md and 07-the-foundation.md.
The immutable 02-DECISIONS records keep their original wording (and links to
them are unchanged) — a term retired here may still appear there, which the
glossary explains how to read.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:48:52 +02:00
jschoubben f9f48fbbf7 Glossary: one name per thing, and the words we retired
Locks the vocabulary that kept drifting in conversation — controller (not
"control plane"), foundation (not "substrate"), node and control-node, seat /
bench / claim, package vs artifact. AGENTS.md points at it as the authority.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:22:31 +02:00
jschoubben b43b60b183 ADR 0076: the SDK is a published package the toolchain resolves by version
Records the decision the package-registry work turns on — the SDK is built on a
public base and published before the toolchain that consumes it, so nothing is
circular; mesh-tools stays the thin toolchain base but resolves the SDK by
version. Reconciles docs 12/17/22 and indexes the record.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 10:28:08 +02:00
jschoubben 17c2e061df Phase 1 was mostly a phantom: correct doc 19 and the WBS
The protocol spec claimed the envelope and grant drifted across implementations.
Inspection showed the wire agrees — envelope required headers match, the two
optional ones are legitimately optional, and the grant wire (the contributions
file) is identical on both sides. The disagreement was in dead types, now removed.

So phase 1's 'make them agree' work is done by deletion and correction. What
remains is a conformance fixture as prevention — pinning the envelope and the
contributions file so a future change that breaks agreement fails a test — and a
full per-capability suite is deferred until a third language actually needs it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 00:07:26 +02:00
jschoubben a40595fa08 ADR 0074: correct the evidence — the live wire agrees
The record claimed the two implementations already disagreed. Inspection showed
the live wire agrees: the disagreeing grant types were dead (removed), and the
envelope's two extra headers are optional and set when relevant, not missing.

The danger was dead types contradicting the wire, not live disagreement — which
is a sharper reason for specifying the wire and checking against it, not a weaker
one. The model stands; the conformance suite's job is prevention rather than
repairing a present break.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 00:06:49 +02:00
jschoubben 6e1099a905 Reorder the work: implement and unit-test first, run the lab once at the end
The correction the operator pushed: stop running a 20-minute lab against a mesh
mid-transformation, debugging paths the next phase deletes. The base-build hang
is almost certainly the SDK resolving from a git URL inside a docker build (issue
053), which Phase 2 removes — so debugging it on the current shape is debugging
deprecated code.

Phase 0 folds in: the installer's own regressions are fixed and committed;
whether it runs green is the final acceptance test, after the phases that change
its build path are in. Faults that can be reasoned out of the code path are, by
reading rather than running.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 00:02:51 +02:00
jschoubben 2e1ae061b5 The work ahead: four phases, dependency-ordered, each ending at a run
Everything decided this cycle and not yet built. Phase 0 gets the installer
green, because nothing else is testable end to end without it. Phase 1 makes the
protocol one thing and fixes the Go/TS drift the installer's own provisioning
exercises. Phase 2 stands up the private package registry ADR 0014 assumes and
publishes the SDK into it. Phase 3 adopts the substrate so one postgres and one
lavinmq serve everything, which is the hardest and needs all three above.

Order is dependency, not preference. Each phase ends at a run rather than a
paragraph, because a phase that ends at a claim is how things went missing this
cycle without anything complaining.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 23:41:42 +02:00
jschoubben 03fc13b84b 051: one lavinmq, not two — the duplication is running, not latent
The lavinmq module raises its own server container and provisions vhosts on it,
separate from the substrate's mesh-broker. A mesh with the module assigned runs
two LavinMQ servers where one belongs — the exact AMQP twin of the two postgres
containers.

The module's own provisioner already assumes one server: it creates a vhost per
consumer, named for the login, isolated by the vhost boundary — the analog of
postgres's database-per-login. So the mesh's own control traffic is the / vhost
and every consumer's broker is a vhost beside it, all on one server.

Adoption therefore means the module does not run a server of its own: its server
is the substrate broker, adopted, and the module contributes the provisioner,
tools, events and bootstrap against it. The isolation model is already built;
what remains to design is only raising the one broker at genesis and then holding
it as a module, over the broker it is.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 22:31:17 +02:00
jschoubben a062f181ce The twelve-module table names distribution, as the catalogue does
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 22:03:39 +02:00
jschoubben 87f464cc5f A finished mesh holds twelve, and two rows were one module each
The substrate's store and the postgres module are the same thing. They were two
rows only while the substrate was a different KIND of thing — a store raised from
a bundle cannot provide postgres-database, so anything wanting a database needed
a second server. It is visible on any mesh built today: mesh-store and postgres,
two containers, the same image.

Which name survives is settled by the naming rule. Where a consumer speaks a
protocol the interface is the protocol, and "database" is not a capability. The
control plane's own queries use distinct on and on conflict, so the coupling is
to postgres and a "store" module would advertise a swap that fails the first time
anyone tries it.

The broker collapses the same way with a different outcome: amqp IS a protocol
that several implementations speak, so amqp is a legitimate provision and lavinmq
is one provider of it.

So adopting the substrate is not only an upgrade path — it is two rows of a
mesh's module list becoming one, twice. And it leaves nothing that is a specialty
after the pivot, which is the claim the whole design rests on and is not true
today for exactly those two.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 20:26:42 +02:00
jschoubben 5a71e8659f The host's unit exists; the installer just does not place it
Reported as 'the host does not survive a reboot', which reads as a mesh that
cannot come back. mesh-host/packaging/ ships nox-mesh-host.service and two
companions. The installer declines to place them because a unit file is a
packaging decision, and the lab starts the host with --host-in-background, which
says in its own help that it does not survive a reboot.

So the lab run failing this was the lab being honest, and the gap is the step
that puts a shipped unit on a machine — narrower and more fixable than what I
wrote.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 20:15:48 +02:00
jschoubben 1e8273eb19 Correct 0075: co-residence is not the exclusivity that matters
The first draft implied gitea and the registry could not share a machine, because
registry claims the-artifact-store at node scope and I carried that across to
gitea without asking what the claim is for.

A machine running gitea for git and packages alongside a registry serving
artifacts is an ordinary arrangement. They are different ports doing different
jobs, and nothing about one being the mesh's artifact store requires the other
not to exist.

The exclusivity that matters is mesh-wide and already expressed: provides at mesh
scope means two providers are two answers, and the resolver refuses until one is
assigned. Forbidding co-residence adds nothing and forbids something reasonable.

Whether registry should still hold that claim is left open rather than answered
from outside its manifest — it may be protecting something about its port or its
data directory that nobody wrote down.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 19:35:05 +02:00
jschoubben 199c2f20bb ADR 0075 — an artifact store is a provision, and a package registry is another
Two questions circling turned out to be one asked twice: should gitea be the
mesh's registry, and where does the SDK come from. The framing that dissolves
both is that artifact-store is already a provision and the registry already
provides it — so this was never about replacing a component. It is a second
provider of an existing provision, which this mesh has a mechanism for and uses
for certificate authorities already.

So: two provisions, because they are two jobs. artifact-store is content
addressed, pinned by digest, no versions and no ranges — what the mesh delivers
to machines. package-registry is an ecosystem's own, addressed by name and
version — what code resolves when compiled. Conflating them is how a mesh that
pins everything ends up rebuilding one commit into two different things.

The small registry stays the provider genesis installs, not because it is better
but because of what it is: a directory and one container, installable where there
is no database and no control plane. Gitea needs both, and the pivot needs
somewhere to publish before either exists.

Gitea also provides artifact-store, so a mesh may choose it — and choosing it
answers issues 042 and 048 by adopting something that already has accounts and
TLS, rather than reimplementing them in a registry that has neither.

Moving between providers is a designed act with a verification step that is easy
to skip and is the only thing between it and a mesh that cannot restart its own
control plane.

And the bootstrap still has no package registry when the first build needs one.
Named rather than solved, so the next person does not discover it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 18:06:03 +02:00
jschoubben 94229d95eb The installation, written out in full
Every step from a bare machine to a mesh that maintains itself, in three phases,
with each step named as the installer prints it.

The point of writing it out is the shape it exposes. The installer owns twelve
steps and ends at a mesh that RUNS. Seven more turn that into a mesh that WORKS —
the shared base, a store that is a provider rather than the control plane's own
memory, the catalogue, the replay of what was built before the catalogue existed,
the control plane rebuilt through the module path, the private network with the
node actually placed on it, and the packet filter. None of those seven is the
installer's. They are things somebody types, which is why a test had to be
written to discover they were missing.

Machines arrive last, in phase three, because a machine joining a mesh that
cannot build anything proves enrolment works and nothing else.

And five things that are not yet true are named rather than implied: phase two is
manual, a second machine cannot pull what the mesh built, ADR 0014 assumes a
private package registry that genesis has not installed when the first build
needs it, the host agent does not survive a reboot, and nothing can contradict a
claim that a machine was installed this way.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 16:51:52 +02:00
jschoubben 27b2161379 The SDK's delivery is decided; the git URL violates it
ADR 0014 is accepted and unambiguous: each module consumes its dependencies from
the private registry, the mesh's own shared library included, and a cross-package
change is publish then consume.

So the git dependency at a pinned commit is not a mechanism under consideration.
It is the shared library being consumed a way the record rules out, and the lock
naming a sibling directory is what that looks like when nobody publishes. Issue
053 is reclassified from a question about mechanism to a violation with a
direction.

What stays open is narrower and real: which software serves the private registry,
and that a fresh mesh has none when the first SDK is built — ADR 0014 assumes one
exists, and at genesis nothing has installed it.

Recorded after arguing at length for a bespoke content-addressed alternative,
against a decision that was already made and that I had not read.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 16:50:40 +02:00
jschoubben bc271d4de0 A worked guide: one module, four capabilities, four languages
What the SDK contains, answered by exclusion as much as by inclusion. It is the
protocol and nothing else — no configuration loader, because configuration
arrives as files the mesh wrote; no API clients, because a Plex client changes
when Plex changes and that has nothing to do with any other module; no storage,
HTTP or logging, because the language has those. The test for anything proposed
is ADR 0039's: does editing it recompile unrelated modules, and does it change
often. Both, and it stays out.

Then the worked module: events in TypeScript, tools in Go, a provisioner in Rust,
a scheduled job in Python. Four artifacts, four toolchains, four processes, one
module — and each part is an ordinary project in its language depending on the
mesh SDK the ordinary way, so a laptop resolves what a build resolves.

And publishing a package as a module capability, which makes the SDK unspecial:
it is simply the first module that published a library. A Plex client belongs to
the Plex module because that is the only thing that knows when Plex changed.

Three things left open rather than papered over: which registry (the catalogue
holds verdaccio and a forge usually serves one too, and nothing says which is
ours), who may publish (a credential that does not exist), and what a range means
in a mesh where everything else is pinned by digest — a mesh that can rebuild a
commit and get a different library is a real change, and should be decided rather
than arrived at.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 14:12:01 +02:00
jschoubben b637fe106f The module protocol, specified per capability
A floor every implementation needs and three capabilities independent of each
other, so an SDK can implement the floor and events and be a real thing rather
than an unfinished one.

Written as a specification, which means it says what is required rather than how
anything is arranged — and says plainly where it describes behaviour that is not
yet true. Three places it does:

x-causation-id and x-schema are specified and emitted by nothing; the Go side
writes four headers and the TypeScript side declares six. A module may serve
tools and may not call them, because a caller needs a reply queue its account may
not declare. And the two implementations disagree about what a grant carries — in
TypeScript consumer is the module, in Go it is the node and the module is From.
One word, two meanings, in two halves of one mesh.

Naming those in the specification rather than leaving them for conformance to
discover, because a specification that only described what already works would
have nothing to say about the things most likely to break.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:45:25 +02:00
jschoubben 033a5d384c Issue 053 — the SDK is pinned twice and the two disagree, and ADR 0074 accepted
The tool runtime's manifest names the SDK as a git dependency at a pinned commit;
its lock file names a sibling directory that exists on one workstation. It builds
only because the recipe runs npm install, which tolerates a lock disagreeing with
its manifest and re-resolves from the manifest — the one command that hides this.

It matters because a lock exists to make a build reproducible and this one
describes one machine, and because it is the first thing a fresh mesh builds: the
toolchain carries the SDK and everything with code of its own compiles inside it,
so a dependency resolved differently on the build machine than on a workstation
is a difference in every module the mesh will ever build.

And it is about to be copied. Each language's toolchain will carry that
language's SDK the same way, so the shape is worth settling before there are four
of them.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:44:02 +02:00
jschoubben 89302aa3e0 The protocol is split per capability, and an SDK implements it
Two corrections, both from the operator and both better than what was written.

An SDK is an implementation of the mesh's module protocol in one language, and
nothing more. The first draft defined it by the test it passes, which describes
how you check one rather than what one is — and leaves it sounding like a library
that helpers could accumulate in.

And the protocol is split per capability, which was missing entirely. A module
that only consumes events uses the event capability; one that serves tools uses
the tool capability; a provider uses provisioning. Nothing about consuming an
event requires knowing how a grant is answered, so an SDK need not implement all
of it to be real.

That has a precedent here: a host declares which resource kinds it can apply, and
a partial host is a real thing rather than a broken one (ADR 0005). An SDK
implementing the floor and events is exactly as legitimate, and a module written
against it is a module that does events.

Which changes what adding a language costs. A Rust SDK doing connection and
events is useful the day it exists, with tools and provisioning following when
something needs them — rather than a language being unsupported until it is
entirely supported, which is what makes adding one a project instead of a
contribution.

Conformance is therefore per capability too: a monolithic pass or fail would make
a partial implementation indistinguishable from a broken one, which is the
distinction the whole thing rests on.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:40:15 +02:00
jschoubben 3d693a8735 ADR 0074 — the wire is specified; an SDK is what passes the suite
ADR 0039 settles what belongs in an SDK. It does not say what happens when there
is more than one, and there already is: the contracts are expressed as Go types
in the control plane and host and as TypeScript types in the SDK, and nobody has
felt it because both live in one repository.

They already disagree. The provision's field is "resource" in one and "Provision"
in the other; "consumer" means the module in one and the node in the other; the
envelope declares six headers on one side and emits four on both — the missing
two being x-causation-id and x-schema, the second of which is exactly what a body
needs in order to change shape without silent misreads.

That class of failure does not announce itself. Two implementations disagreeing
about an envelope do not fail to compile — they ignore each other's messages, and
a mesh where a module stops reacting looks like a mesh where nothing happened.

So the decision is to specify the wire rather than share the types, because the
shapes are the easy half. What two implementations actually disagree about is
behaviour: queue naming and durability, which headers are required and what an
unknown one means, taking identity from the sealed credential rather than the
environment, dedup on an id only the emitter can make, pinning a fingerprint
rather than trusting an authority.

And the suite is executable rather than prose, because a specification nobody can
run is a document two implementations drift from while both believe they conform.
The two existing implementations are the first made to pass it — a suite only new
SDKs must satisfy would certify every future language against a disagreement that
is already here.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:26:57 +02:00
jschoubben 411f0680b8 Document the whole module surface, and keep it true with a test
A reference table goes stale the day somebody adds a field, so this one points at
mesh-catalog/modules/showcase — a module that uses all of it — and a test that
fails when it stops doing so. Read the module when the table disagrees with it.

Two rows in the coverage survey were stale because of this week's work: systemd
units were a file plus a service, which made every author write unit syntax and
is why "process" exists; and building from source was images only, where a
bundle now names a language and lets the mesh choose the toolchain.

And the two rows at the bottom of the resource table are the interesting ones.
"action" is refused to modules outright — the link may not carry a command, so a
module needing something done ships a program that reconciles. "service"
installs no unit by design, right for software shipping one and wrong for code
the mesh built, which has none until the mesh writes it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:06:16 +02:00
jschoubben 8a7328c282 Correct the design: archives already work, compiling is what is missing
The first draft said the builder refused archives. It does not. An archive is
packed deterministically, hashed, published by digest, fetched by the machine and
unpacked — the whole path exists. Only the local builder used at genesis refuses
one, and deliberately: an archive is bytes that mean nothing until something
serves them, and at genesis nothing does.

What an archive cannot do is compile. Its source is a directory packed as it
stands, so shipping compiled output means compiling somewhere first, which means
a Dockerfile — the burden this document is about. The gap is not the artifact
kind. It is that no recipe both builds and packs.

Found by reading the builder rather than the manifest schema, which is where the
first draft's claim came from.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 01:54:43 +02:00
jschoubben 5d1e6d0b09 Design: building a module, and why the recipe cannot always be a Dockerfile
12-a-module-repository says what a module may build and where it goes. Nothing
said how a build is MODELLED, and the model is the problem: a recipe is implicit,
singular and always a Dockerfile; a toolchain is not modelled at all, arriving as
two build arguments the module hand-writes; a language is not a concept; and an
archive is declared in the manifest and refused by the builder.

The cost is measurable rather than theoretical. Adding a module with its own code
means repeating an incantation - two ARG bases, a specific working directory so
the SDK resolves upward, the compiler invoked by absolute path because the usual
symlink is resolved away when the base is assembled, a second stage, an env var
naming the entrypoints. Most of the catalogue is unconverted, and two conversions
done in one session were each wrong twice with a working example open.

So: recipe becomes explicit with three kinds, and toolchain becomes derived from
a declared language rather than written by every author. A Dockerfile stays, and
stops being compulsory - it is right for software needing a particular base and
wrong for "compile my module's code", which is the same operation every time.

The cost is stated before it is chosen: every language is permanent, and the
contracts are already expressed twice - Go structs and TypeScript types kept in
step by hand. A second language makes that drift. So language-neutral contracts
come first, or the drift gets worse while hiding.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 01:51:42 +02:00
jschoubben b163ed1fcc Issue 052 — the firewall closes the port the mesh runs on
The packet filter generates its rules from what modules declare they listen on.
The broker is not a module, so it declares nothing, so its port is not opened.
Every machine dials that port to enrol and to receive every declaration it is
ever sent.

Invisible where it is assembled and fatal on the next machine: a mesh of one
never dials its own broker across the network, so the ruleset looks right. The
first machine to join is refused at the packet filter during enrolment, several
steps from anything that reports it — and assigning the firewall before joining
machines is both the natural order and the one that breaks.

This is 051 in a second place. That issue says the substrate cannot be updated
because the mesh holds no record of it; the same absence means the firewall
cannot know it exists. Ssh is already a floor for the same reason — a machine
nobody can reach is a machine nobody can repair — and the broker may be the same
class of fact.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-14 22:59:33 +02:00
jschoubben f52895a646 Issue 051 — the mesh can update everything except what it depends on
The store and broker come from a bundle the installer writes once, with images
pinned in it, and nothing can change them afterwards: no build, no version to be
behind, no roll-out, and no way to report being out of date, because the mesh
holds no record of them as modules at all.

That is backwards. They are what everything else depends on, so their updates
matter most, and they are the only things with no mechanism to deliver one. A
mesh with a year-old broker reports itself entirely current.

The fix probably already exists: the control plane is carried, raised and then
adopted as an ordinary module pinned to what is running. Nothing in that pattern
is specific to the control plane. It would also remove a duplication visible on
any one-node mesh — the same postgres image running twice, because a store that
cannot be a module cannot provide a database to anything.

Recorded with the two hard parts stated rather than waved at: upgrading a store
the control plane is reading from, and upgrading a broker over the broker.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-14 22:44:10 +02:00
jschoubben 5d13f9c83d Issue 050 — the catalogue knows nothing built before it started
A mesh raised from bare metal built six modules and its catalogue reported three:
exactly those built after it began running. Missing were the shared base, the
store, and the catalogue itself.

The hole is never random. On a fresh mesh the modules built before the catalogue
are by necessity the ones it needed in order to exist, so the foundation is
always what is absent, on every mesh, at the moment the graph is first populated.

It breaks the question the catalogue is for: build edges hang off the base, so a
catalogue with no record of it answers 'what must be rebuilt' confidently and
wrongly. And nothing reports the gap, because a catalogue cannot know what it was
never told — this was found by comparing its answer against what the mesh had
just been watched doing.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-14 22:16:23 +02:00
jschoubben 0aa8f62e0c Issue 049 — a module serves tools and nothing may call them
A module's broker account is scoped to what it declares it emits and consumes. A
tool call needs a reply queue, which that scope does not cover and should not. So
the account is right, the request is reasonable, and no account exists that can
make it — asking a module its own question, from its own container, with its own
credential, is refused.

It matters because a module's tools are its operator-facing surface: the
catalogue serves the five questions it exists to answer and nothing can reach
them. It is also why a running module keeps being mistaken for a working one — a
test that cannot ask anything checks a container is up, and that substitution has
hidden two faults this week.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-14 22:08:40 +02:00
jschoubben ee70d5b451 Issue 048 — a stated rule about the registry is enforced by nothing
The registry says every machine pulls from it and opens its port to the mesh for
that reason. A machine that tries is refused by its own container runtime: the
registry serves plain HTTP and anything but loopback is treated as HTTPS.

It has never failed, and that is the finding. Every proof that a machine can
fetch a mesh-built artifact was a proof about the machine that built it, where
the reference was loopback. The bed that uses a routable address gets away with
it because the harness writes the runtime's configuration before the mesh exists.

Kept separate from 042, which they are easy to confuse: that one is about not
being allowed to pull, this one about not being able to whatever the credential
says. Fixing 042 alone leaves a machine with a valid account for a registry its
runtime will not talk to.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-14 21:01:32 +02:00
jschoubben 3121e47245 Merge pull request 'Issue 047 — and the half that runs the other way' (#42) from issue/047-the-other-half into main 2026-09-14 15:09:36 +02:00
jschoubben 261064ec63 Issue 047 — and the half that runs the other way
The report covered ports the firewall cannot close. The matching fault is ports
it does close and should not: rules come from what modules declare, a migrating
mesh knows about almost nothing, and anything listening on the host is dropped.

No module declares an ssh port and the generator has no allowance for one. The
session that loads the rules survives on conntrack until it drops, and then the
machine is reached from a rescue console.
2026-09-14 15:09:20 +02:00
jschoubben 3f35153dd5 Merge pull request 'Issue 047 — the firewall does not cover published container ports' (#41) from issue/the-firewall-does-not-filter-published-ports into main 2026-09-14 14:59:31 +02:00
jschoubben 027c41d125 Issue 047 — the firewall leaves published container ports open
Rehearsed on three lab machines before doing it on an anchor: loading the mesh's
rules refuses an ordinary host port and leaves a published container port
reachable, same prober, same second.

It is deliberate — no forward chain, because dropping there would stop every
container — but traffic to a published port never reaches the input chain, so
the firewall is silent about it. The anchor publishes 38 such ports today, all
filtered by the system being replaced, so the cutover would open every one of
them while reporting a firewall that is up.
2026-09-14 14:59:11 +02:00
jschoubben 9359542990 Merge pull request 'Issue 046 — an upstream image cannot be mirrored into the mesh's registry' (#40) from issue/mirroring-an-upstream-image into main 2026-09-14 13:27:59 +02:00
jschoubben 24d52a8380 Issue 046 — an upstream image cannot be mirrored into the mesh's registry
Found while migrating the first module. Five approaches were tried and observed
to fail, including resolving the index and naming the platform at both ends; a
speculative fix was written and reverted rather than shipped, because it did not
make the mirror work.

One module uses this today, which is why it went unnoticed — and it is the shape
most of a migration wants, because the services being moved are third-party.
2026-09-14 13:27:42 +02:00
jschoubben e773bf4c34 Merge pull request 'Installing ends with a builder, and the record says so' (#39) from feat/installing-ends-with-a-builder into main 2026-09-14 12:34:43 +02:00
jschoubben 256884e671 Installing ends with a builder, and the record says so
The design said how the builder arrives was unsettled and that nothing
installed it — the one gap stopping a fresh mesh from producing anything. Both
are now false. What is still true is narrower and worth keeping separate:
nothing asks a raised mesh for the rest of the catalogue, and no bed asserts
that it could.
2026-09-14 12:34:26 +02:00
jschoubben aa35ba1594 Merge pull request 'A module names its base, so the mesh can act on what it notices' (#38) from feat/a-module-names-its-base into main 2026-09-13 23:58:23 +02:00
jschoubben 2eb65a17a3 A module names its base, so the mesh can act on what it notices
The mesh could already say which modules a base change invalidated, and could
not do anything about it: each recipe named one particular copy of the base by
fingerprint, and rebuilding produced the old one. Worse, the copy each named
existed only inside a throwaway lab, so those three modules could not be built
anywhere at all — and the line each replaced had the same fault.
2026-09-13 23:58:06 +02:00
jschoubben 68d83c5b24 Merge pull request 'Two graphs, the builder's arrival, and two findings from building on a live mesh' (#37) from feat/two-graphs-and-the-build-chain into main 2026-09-13 11:17:48 +02:00
jschoubben 8e7fac8bf1 Record what genesis now does, and how each rule is checked
The builder's arrival was the one rule the document said nothing checked. It is
checked now, by both genesis beds — and so is the thing that distinguishes a
built control plane from a carried one, which every earlier assertion accepted
either way.
2026-09-13 06:09:31 +02:00
jschoubben fdc61054e3 Genesis carries a builder, and the document says so
The section saying the change was decided and had not happened now contradicted
the section below it. It also records the argument that failed, because a reader
will otherwise ask the same question and reach the same wrong answer.
2026-09-13 04:26:01 +02:00
jschoubben aabaaf2bb4 Rewrite 0073: the registry does not move, and the argument fails
Written an hour ago claiming a produced image must be published before anything
can fetch it, so the registry had to precede the control plane. The premise is
false: the machine that builds the image is the machine that runs it, and the
temporary control plane names a built image exactly as it names a carried one —
by the digest of its own configuration, which requires nothing to have served
it. Building changes where the bytes came from, not where they are.

Rewritten rather than superseded because nothing has been built on it and
nobody has read it: a record that contradicts itself is a draft, not a decision.
The argument is kept, because it was asked for and a negative answer is the
result.
2026-09-13 03:22:05 +02:00
jschoubben 9f5ac38662 Settle how the builder arrives, and what it publishes into
The two questions the design record named as the one gap stopping a fresh mesh
from producing anything. They cannot be answered apart: a builder with nowhere
to publish has made a file on a disk.

The registry's role did not change — the answer to 'must it precede the control
plane' did, because the control plane's image is now produced rather than
carried, and a produced image must be put somewhere before it can be fetched.
2026-09-13 03:20:07 +02:00
jschoubben 3983188b8a Issue 044 resolved — the mesh builds its own floor
Cloned alone, built by the mesh, every module moved onto it, and the base then
changed for real: all three went stale naming what moved. A comment-only change
correctly makes nothing stale, which found a second bug — staleness compared
commits where it should compare artifacts.
2026-09-13 02:51:08 +02:00
jschoubben 15ac7fd8dc The base has no circularity — the builder does not stand on it
Written as an open question; it has an answer, and leaving it open would have
made the fix look harder than it is.
2026-09-13 02:25:07 +02:00
jschoubben 3a9ed9d3fd Two findings from building the catalogue on a live mesh
The runtime every module compiles against cannot be built by the mesh, so the
one rule that would catch it moving can never fire. And a container keeps the
values it was created with, so two good applies can leave it running on neither.
2026-09-13 01:49:02 +02:00
jschoubben a4866ca899 Merge pull request 'Two graphs, and a build chain that orders itself' (#36) from feat/two-graphs-and-the-build-chain into main 2026-09-12 23:09:42 +02:00
jschoubben 58ad0742d8 Two graphs, and a build chain that orders itself
Corrects ADR 0070, written an hour earlier, which had the control plane consuming
the catalogue in order to compose a declaration. That was written before the two
graphs had been told apart and creates a dependency that need not exist: a
catalogue that is down would leave the control plane unable to compose the thing
that would repair it.

The catalogue links module-versions to each other and does not know nodes exist.
The control plane links module-versions to nodes, and holds capabilities and
claims. They meet only when something is installed, and everything between them
travels as events over the broker, on durable queues, so nothing is lost when a
receiver is away.

Build order is not computed anywhere. The builder never consults the graph and
builds what it is asked for; the catalogue asks for the next build after the
previous registration, so ordering holds by construction. Its rule is a condition
rather than a schedule — rebuild once everything a module was built against is
current — which covers a chain and a diamond alike.

Left open: whether an upgrade is applied or merely noticed, and whether a module
on several machines upgrades on all at once.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 23:09:23 +02:00
jschoubben 90f7bcb209 Merge pull request 'Genesis clones from a mesh, and checks what it got' (#35) from feat/where-genesis-gets-its-source into main 2026-09-12 22:20:28 +02:00
jschoubben 72b0810c53 Genesis clones from a mesh, and checks what it got
ADR 0070 has the init builder clone the source and does not say from where, and
ADR 0067 had rejected building at genesis partly because the forge runs on the
mesh being rebuilt. That objection binds only when those are the same mesh, which
is true exactly once.

So genesis clones from a mesh by name, and if that mesh is lost the name moves to
another that holds a copy — recovery is a name pointing elsewhere rather than a
backup being restored, and every installation adds somewhere it could point.

It names a commit and checks what it got, because the forge a mesh installs from
is the trust anchor for everything that mesh will run. On 2026-09-11 that forge
was running a cryptominer and tampering with git operations in flight; nothing was
altered, but a mesh installing during those hours could not have known that.

What relationship a mesh keeps afterwards is left open and named, so that whoever
writes the init builder does not settle it by accident: a snapshot and then
independence, or a continuing upstream for core modules.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 22:19:41 +02:00
jschoubben 03c73e7698 Merge pull request 'The catalogue owns the module graph, and genesis builds rather than carries' (#34) from feat/the-catalogue-owns-the-module-graph into main 2026-09-12 22:09:38 +02:00
jschoubben 64ae75a480 The catalogue owns the module graph, and genesis builds rather than carries
The graph had no owner: what modules are, what they require, what they claim and
what they are built against all sat in the control plane because that is where it
was written first. The control plane's own test says otherwise — anything a single
machine could answer alone is not its work, and what a module needs requires no
knowledge of any node.

So the catalogue becomes a core module beside the control plane and the builder,
owning the graph and serving tools over it. The control plane consumes it, which
is the opposite of what the tiers suggest and is therefore stated rather than
inferred.

That makes the catalogue a fourth thing that cannot arrive through the ordinary
path, so the claim written this morning that the list was closed at three is
corrected. All four are answered by one mechanism instead: the installer carries
an init builder and the core modules are built on the machine, so what is carried
is a builder rather than a result and nothing is left without a route.

Two things are open and named rather than assumed: where the init builder clones
from, given the forge normally runs on the mesh it would be rebuilding, and what
it publishes into, given the registry is installed later in the order today.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 22:03:05 +02:00
jschoubben 0fa4d14f54 Merge pull request 'A module is a repository and a path, and installing is described to its end' (#33) from feat/a-module-is-a-repository-and-a-path into main 2026-09-12 16:54:22 +02:00
jschoubben 837b5df2f7 Withdraw 043: the capability existed and the wrong verb was used
A build machine was refused the build queue, and this was raised as a gap in what
a manifest can express. It is not: `builder issue` creates exactly that account,
three lines from the code being read at the time.

Kept rather than deleted, for the one real thing in it — the wrong verb succeeds
and reports success, producing an account that authenticates and can do nothing,
so the failure surfaces a layer away as a permissions error that reads like a
missing feature.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 16:51:37 +02:00
jschoubben 9da45c68d6 Propose that the lab takes requests, one at a time, from its own copy
Raising a scenario occupies the machine and the person who started it, and
running in the background against a working copy is worse than waiting: a run
reads that copy as it goes, so editing while it runs yields a result describing a
state that never existed.

Proposed rather than accepted. The load-bearing part is the restriction — a
request names a bed and a commit and nothing else — because a request that could
say what to install and where would make the lab a second way of installing a
mesh, which is the arrangement that just cost a year of late-found faults.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 16:51:37 +02:00
jschoubben ddf104f8aa A module is a repository and a path within it
The design said a module's manifest sits at a repository's root, full stop, which
means one repository per module. Nothing that exists is shaped that way: the
catalogue holds sixty-seven modules one to a directory, no code repository has a
manifest at its root, and the system being replaced has always built a module
from a repository and a path.

So the builder could be asked to build nothing that exists — pointed at the
catalogue it finds no manifest, pointed at a module's source it finds none
either. Recorded as a decision because it moves the core modules' manifests
beside their source, and corrects the design that said otherwise.

Also corrects, in the same document, how the three things the build loop cannot
produce actually arrive. They were written as though all three were carried in.
Only the control plane is: the registry is pulled from the public internet, and
the builder has no route at all — which is now stated as the open one rather than
implied to be solved.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 16:51:37 +02:00
jschoubben 3d939b5c77 Describe how a mesh is raised, because only a test did
The one complete account of standing a mesh up was an integration test, and a
fixture is free to invent what it needs — which is how a registry that exists in
no production hid two faults for as long as the lab existed.

Written from what the installer does, not what it should do: genesis and joining
are separate moments, the lab runs the installer rather than describing
installing, and three things that are not true yet are named rather than glossed,
including one rule nothing checks.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-12 16:51:37 +02:00
jschoubben c567410687 Merge pull request 'Genesis is a pivot, public routing is name-agnostic, and five issues the fake registry was hiding' (#32) from design/bootstrap-is-a-pivot into main 2026-09-11 21:56:49 +02:00
jschoubben 26bec28c60 Issue 042 — nothing gives a node an account for a registry
Found by deleting the lab's registry and giving the machines a real path out:
public images fetched, the operator's own could not be fetched at all. There is
no provision for a registry credential, no manifest field, and no step in
enrolment that establishes one.

It applies to the mesh's own store too, which today asks for nothing — a
decision that has never been written down as one, and so reads as an absence.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 01:36:00 +02:00
jschoubben cde00e1d5f 0054 and 0055 join the topic their subject already had
Both carried 'model access', which is not one of the six the reading order
knows, so neither had a place in it. 0050 — the record they extend, on the same
subject — is 'what runs on it'.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 01:06:59 +02:00
jschoubben c29accd1be Issue 039 resolved — the operator's images are pinned, and why that is a stopgap
Nine references now name the digest their tag resolved to. What closed is the
immediate fault; the open questions stand, because a digest in a repository is
wrong the moment anybody rebuilds — which is the reason the design wants the
repository to name artifacts and the mesh to hold digests.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 01:06:35 +02:00
jschoubben 47dc3bc8e5 Accept ADR 0066 and ADR 0067
0066 was proven on a four-node bed before it was ratified: one node setting
moved an entire domain, a routed name resolved inside the mesh and was issued a
certificate by the internal authority, and TLS verified against that authority
with no override. 08-connectivity rests on it and could not while it was
proposed.

0067 records what deleting the lab's registry exposed — that pinning quietly
required a registry before the thing that lets a mesh have a registry could
start — and the pivot that resolves it.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 01:04:21 +02:00
jschoubben 423fde8534 Place the two new records in the reading order, and fix a link the merge moved
Both carried a topic outside the six the index knows, so neither had a place to
be read in — 0067 had none at all. Both are 'the tiers', beside 0036 (bootstrap
ends at a usable mesh) and 0007 (connectivity), which is what they extend.

0067 cited 0041 for tier 0's property; on this trunk 0041 is events, and the
record it meant is 0005. A citation that resolves to the wrong record reads as
corroboration, which is worse than a dead link.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 00:48:20 +02:00
jschoubben 56d0dcc4ff Merge branch 'feat/routing-is-name-agnostic' into design/bootstrap-is-a-pivot 2026-09-11 00:46:04 +02:00
jschoubben 84c95c53af Merge branch 'issue/021-provider-port-published-on-loopback' into design/bootstrap-is-a-pivot
# Conflicts:
#	03-DESIGN/01-to-be/04-lab-installation.md
2026-09-11 00:45:59 +02:00
jschoubben e4a5d9b1d0 Lab installation: the reachability check joins the assertions it belongs with
The doc's own rule is that the lab verifies capability by outcome, never by
reading a setting. A path out is exactly that kind of claim — a route and a
policy can both read correctly while nothing gets through — so it is asserted by
fetching something, in the table with the rest.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 00:11:39 +02:00
jschoubben 1ce1c3d4e0 Lab installation: a path out, and why the daemon having one is not enough
A container runtime on the same workstation sets the kernel's forwarding policy
to drop, and the virtualisation daemon's own accept rules do not override it —
both are consulted and a drop anywhere is the answer. The machines then get
addresses and resolve names, because the daemon's resolver is on the bridge, and
discard every packet to anything real.

The sharpest 'available is not adequate' yet: nothing is misconfigured, nothing
logs, and it presents as every image pull hanging. A workstation that runs
containers is the ordinary case, so it is a prerequisite — verified by reaching
something, never by reading a setting.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 00:11:22 +02:00
jschoubben 8f23d4b114 0067: the artifact store may require nothing, not merely build nothing
029 says a module providing the artifact store may not build artifacts. The
pivot shows that is the narrow case: it may not require anything the store is
needed to deliver. A route-label migration gave the registry a public name and
a route requirement, and at genesis nothing provides a route — nor can anything,
since the routing stack needs images and images need the store.

The same cycle through a door the existing wording did not cover, so the rule is
widened where the bootstrap decision states it, with a check that would catch the
next one where it is written.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 00:10:39 +02:00
jschoubben f9e129f734 Issue 040 — the only description of how a mesh is stood up is a test
There is no installer. The complete account of standing up a mesh is an
integration test in the lab, and a fixture may invent what it needs — this one
raised a registry no production has and rewrote every image reference through
it, concealing both 039 and the fact that a first node outside the lab had no
bootstrap path at all. The bed being green said nothing about whether a mesh
could be installed.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 23:34:59 +02:00
jschoubben e228355a52 Issue 039 — the lab's registry was pinning what the catalogue left unpinned
Nine images across seven modules name a tag, not a digest. ADR 0006 forbids it
and the host refuses it by name — and the refusal has never fired in a bed,
because the lab pushed every image into its own registry and rewrote every
reference to the digest it had just assigned. The harness was supplying the
property under test. Found by deleting the harness.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 23:20:10 +02:00
jschoubben cd1653bfe9 ADR 0067 — genesis is a pivot through a temporary control plane
The control plane's image is built from source and pushed nowhere, so it has no
manifest digest; a registry assigns those. Pinning therefore required a registry
before the thing that lets a mesh have a registry could start — a dependency the
rule created by accident. The lab hid it by raising a disposable registry no
production has.

Proposed, not accepted.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 23:19:17 +02:00
jschoubben 5852b35ab9 Renumber the routing record to 0066 — 0056 was already taken
0056 is 'the authority is the control plane, not a database', drafted on the
in-progress record chain this branch was cut from before those three records
landed. Two files would have collided at merge, which is the kind of thing that
is cheap now and confusing later. The code written against it still says 0056
and is corrected separately.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 23:17:20 +02:00
jschoubben a4440c9acf Issue 038 resolved — corrected root cause (same-node served-port mismatch, not loopback) + fix reference
The real defect is same-node consumers announced the declared port instead of
the assigned/published one; the loopback observation was a stale pre-0038 build.
Fixed in mesh-control fix/same-node-provider-announced-port (c147a26) with a
regression test.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-09 23:06:58 +02:00
jschoubben 9ffe9e97c9 Renumber to issue 038 — 021 is already taken (same-node credentials, closed) and issues run to 037 across branches
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-09 22:46:11 +02:00
jschoubben d16b5294f6 Issue 021: narrow to mesh-assigned ports (bare decl binds loopback, explicit host mapping binds 0.0.0.0)
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-09 22:35:06 +02:00
jschoubben 315bd7118a Issue 021 — a provider is announced at a name its port is not bound to
A from:mesh provider is announced (per 018's fix) at the node's private-network
name, but its port is published bound to loopback, so consumers dialing the
announced <node>.internal:port reach nothing. Diagnosed from the lab: DB
consumers that require the database at startup crash-loop; the provider is
healthy on loopback only.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-09 22:29:04 +02:00
jschoubben e2afb3e144 ADR 0056 + connectivity: public routing is name-agnostic, resolved in-mesh, internally certifiable
A route contribution carries a label; the node carries its public domain; the
mesh composes <label>.<public-domain> and holds no name map. A granted route is
published into internal resolution so anything in-mesh (notably an internal ACME
authority) can resolve and reach it. That authority certifies routed names by the
same path a public one would, differing only in issuer and trusted root.

Records the decision as proposed and amends connectivity SS2/SS3/SS5 plus its Open
list with the lab findings behind it.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-09 22:24:43 +02:00
jschoubben 450a4d8605 Merge pull request 'ADR 0055 — model access is answered by a licence or a node that hosts the model' (#31) from feat/adr-0055-node-model into main 2026-09-07 04:39:25 +02:00
jschoubben 9dfa5ba2ac ADR 0055 — model access is answered by a licence or a node that hosts the model
Extends ADR 0050: model-access, a provision, may be answered by a NODE that
hosts a model (Ollama/vLLM serving an OpenAI-compatible endpoint) as well as by
a licence record. A node-answer delivers an endpoint (base URL + model, and a
key only if the server wants one), rides the ordinary serves/binds path, and
uses no adapter; the resolver already prefers a local model over a licence. One
provision, two answers — the mesh's own model sits behind the same interface as
a vendor's. Names the mesh-scope-alongside-licence limitation and its fix.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-07 04:39:10 +02:00
jschoubben b7a33a1e26 Merge pull request 'ADR 0054 — model usage is a vendor-neutral record at two grains' (#29) from feat/adr-0054-model-usage into main 2026-09-06 23:39:11 +02:00
jschoubben ec2d73bc06 ADR 0054 — model usage is a vendor-neutral record at two grains (licence + session)
Closes the usage-tracking half ADR 0050 left open. One row shape (0050's), recorded at two
consumer grains: the holding module (licence-level, e.g. Anthropic utilization%) and the agent
session (per-session tokens/cost, since a session IS a consumer per ADR 0026). Produced by the
vendor adapter; read on a schedule (0053); recorded as events the audit-logger keeps (0041/0042)
plus a queryable usage context store (0008); usage is not a credential and is recorded in the
clear. Reuses the session, schedule, event, and store the mesh already has. Accepted per the
user's choice to build the full feature incl. per-session token/cost.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-06 23:38:54 +02:00
jschoubben a361c86c3c Merge pull request 'ADR 0053 — a scheduled step is a container run on a recurring schedule' (#28) from feat/adr-0053-scheduled-task into main 2026-09-06 13:53:11 +02:00
jschoubben 12a311ba4c ADR 0053 — a scheduled step is a container run on a recurring schedule
The recurring twin of run-once (0052): a schedule modifier on the container shape, reusing
its security bound (no new host action/shape, strictly less than an action) and reversing its
gating rule — a scheduled step runs after convergence, does not gate the apply, and a failed
run is logged, not fatal. Unblocks kometa's sync and pollers. Accepted per direction to build
the primitive now.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-06 13:52:55 +02:00
jschoubben 897626a9f6 Merge pull request 'ADR 0052 — a step that runs once before a container (issue 037 / run-once primitive)' (#27) from feat/adr-0052-run-once-lifecycle into main 2026-09-06 00:05:04 +02:00
jschoubben a3b0e68ec5 Accept ADR 0052 — a step that runs once before a container
The run-once lifecycle primitive: run-once:true on the existing container shape,
gated by declaration order + exit 0, idempotent by digest. No new host shape, no
arbitrary command — strictly less powerful than an action. Resolves issue 037.
Verified sound and faithful (ADR 0005/0047); status proposed->accepted.
2026-09-06 00:04:21 +02:00
jschoubben 2405d72fb0 ADR 0052 (proposed) — an init step is a container run once to completion
A module can declare state but not a step that runs. mosquitto must seed its
dynsec admin into dynamic-security.json before the broker starts, or the plugin
aborts; the database providers need the same for first-boot migrations and
health gates (04-ISSUES/037). The old event-hook engine that did this was
powerful and flaky; this is the narrowest sound mechanism instead.

A run-once step is an ordinary container marked `run-once: true`: the host runs
it to completion, requires exit 0, and gates the apply on it — so what the
declaration places after it (the broker) starts only once it has finished.
Gating is by declaration order, not a resolved dependency (ADR 0005); the
completion marker is the recorded declaration digest (ADR 0018), so a re-apply
does not re-run it unless the declaration changed. No new host shape and no
arbitrary host command: strictly less powerful than an `action`.

Points 04-ISSUES/037 fixed-by/amended-design at the record; index regenerated;
records.py and index.py pass.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 23:52:26 +02:00
jschoubben c41deb55d2 Merge pull request 'ADR 0051 — shared data is the operator's; a module is granted access (fixes issue 036)' (#26) from feat/adr-0051-shared-data-operator-owned into main 2026-09-05 22:29:44 +02:00
jschoubben d5e12c82aa Accept ADR 0051 — shared data is the operator's; a module is granted access
Your decision, ratified: the media library (and shared/pre-existing data) is
operator-owned and external; a module declares access, not ownership; the host
mounts but owns nothing (no create/chown/reconcile/remove); an absent accessed
path is refused clearly; several accessors co-resolve. Status proposed->accepted;
index regenerated (records + index checks pass). Implementation lives on the code
branches (mesh-control/catalog/host), held for merge after the convergence fix.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 22:29:24 +02:00
jschoubben 220e3c72f5 ADR 0051 (proposed) — shared data is the operator's
Resolves 04-ISSUES/036: eight media modules each declared the shared
library and download directories as their own resources, and the
resolver's duplicate-owner refusal — right in general — would refuse the
stack's only sensible assignment the first time two landed on one node.

The decision, from the operator: shared, pre-existing data is
operator-owned and external. The mesh does not create, chown, reconcile
or remove it. A module declares it needs access to such a path (read or
read-write); the host mounts it and owns nothing. Several modules
accessing one path is normal — the duplicate-path refusal is about
ownership, not use. An accessed path absent at apply is refused clearly,
not created. Extends ADR 0030: the third case the host had no word for,
what it neither made nor configured and must not touch.

Point 036's fixed-by/amended-design at the record; mark it located in
mesh-control, mesh-catalog and mesh-host. Regenerate the decision index.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 22:22:30 +02:00
jschoubben 4430cc1748 Merge pull request 'ADR 0050 (proposed) — model access is vendor-agnostic [awaiting ratification]' (#25) from feat/adr-0050-vendor-agnostic-model-access into main 2026-09-05 21:42:28 +02:00
jschoubben 860d512e91 Accept ADR 0050 — model access is vendor-agnostic
Verified and ratified: model-access stays one vendor-blind provision; per-vendor
adapter keyed by licence.vendor (mirrors public-dns registrar providers); the
sealing-vs-central-rotation carve-out bounded to refreshable-grant vendors /
refresh token / manager node only. Status proposed -> accepted; index regenerated
(records + index checks pass); design doc note updated.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 21:42:20 +02:00
jschoubben 3a9b47918d ADR 0050 (proposed) — model access is vendor-agnostic; amend 14-model-access
Turn the completed vendor-agnostic analysis into HQ design. The model-access
provision stays one vendor-blind interface (extends 0024/0027); the
vendor-specific lifecycle moves into a per-vendor adapter keyed by the licence's
`vendor` field, mirroring registrar-scoped public-dns providers (0044), named at
the consumer's real coupling per 0040.

The crux is the sealing-vs-central-rotation carve-out: for refreshable-grant
vendors only, the manager node holds the refresh token encrypted at rest (a
bounded, declared exception), access tokens sealed per holder, refresh stripped
on delivery. Static-key vendors keep full sealing.

Amend 03-DESIGN/01-to-be/14-model-access.md with the adapter generalisation as a
proposed section (prose + diagram, no code); regenerate the decision index.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 13:43:01 +02:00
jschoubben 3b0aff7585 Merge pull request 'Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work' (#24) from reconcile-init-into-main into main 2026-09-05 12:27:11 +02:00
jschoubben 495db6dc89 Restore initialization's issue 003 — the status flip was based on a wrong premise
Init's 003 was already resolved with its own consolidated attribution
(03-DESIGN/01-to-be/08-connectivity.md); the re-homing overwrote it with the
session's ADR-0045 firewall attribution. Init is canonical and the firewall
decision is recorded in the ported ADR 0045 regardless, so 003 is restored
untouched. No initialization record is modified by this reconciliation.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 12:26:23 +02:00
jschoubben e269f9a185 Re-home this session's new ADRs (0039-0049) and issues (032-037) onto the consolidated scheme; flip issue 003; port repos.md sdk line + feature-branches playbook (07); regenerate index
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 12:24:07 +02:00
jschoubben 546caa31c4 Merge initialization into main — adopt its consolidated structure as canonical
The real work lived on initialization (consolidated decisions 0001-0038, the
fuller issue set 001-031, the control-plane/substrate/node-lifecycle/delivery
design, research 011/012, the checks tooling). main had diverged onto a stale
base and only carried this session's genuinely-new work. This merge makes
initialization's tree canonical on main; this session's 11 new ADRs and 6 new
issues are re-homed on top in the following commits. initialization is recorded
as a parent so its history is preserved in main's ancestry.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 11:59:01 +02:00
jschoubben db8be15a15 Merge pull request 'Issue 013 — a module cannot run its own code at a lifecycle phase' (#23) from feat/hq-issue-013 into main 2026-09-05 11:16:27 +02:00
jschoubben 9c8c4a3ee5 Issue 013 — a module cannot run its own code at a lifecycle phase
Surfaced converting the catalogue: a module can declare things that exist
(dir/file/network/container) but not a step that runs at a point in its
lifecycle. mosquitto's dynsec admin client must be seeded before first start;
the DB providers have nowhere for a migration or health-gate; it is the timing
face of issue 011. Framed as a missing module capability, not a defect. Records
the prior-art event hooks and their real warning — powerful but complex and
flaky — so the resolution avoids rebuilding that. Ends in open questions
(run-once resource vs general lifecycle hook, where the code runs, idempotency).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 11:15:53 +02:00
jschoubben 600a57aafb Merge pull request 'Issues 011 and 012 — what the manifest review could not fix' (#12) from issues/manifest-review into main 2026-09-05 03:52:32 +02:00
jschoubben 2c8616b650 Renumber manifest-review issues 008/009 -> 011/012
Numbers 008 and 009 were taken on main after this branch was opened
(008-provider-runtime-has-no-seal-key, 009-runtime-config-change-does-not-restart,
both merged). Renumber the seed-file-wipe and shared-directory issues to the next
free numbers so merging records two more issues rather than duplicating two.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 03:52:02 +02:00
jschoubben 69fa79c585 Merge pull request 'Playbook 06 — one feature, one branch, one MR per repo' (#22) from feat/feature-branch-workflow into main 2026-09-05 03:47:25 +02:00
jschoubben cabe873503 Playbook 06 — one feature, one branch, one MR per repo
Records the branching-and-merging workflow for code changes across the mesh
repos, written against a failure it names: branches and MRs opened per unit of
thought, treated as done when opened not merged, and named differently per repo,
so they pile up unmerged — one session left sixteen to consolidate by hand. The
rule is one feat/<slug> shared across every repo a feature touches, isolated in
.work/<slug>/<repo> worktrees off main, pushed and opened as one MR per repo only
when the whole feature is done, then merged promptly. Adds the ground-rule
pointer in AGENTS.md and the row in the process overview.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 03:40:39 +02:00
jschoubben 36936dc7a3 Merge pull request 'Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract' (#21) from worktree-issue-provider-seal-key into main 2026-09-05 03:02:34 +02:00
jschoubben bfa08742f8 Consolidate hq: ADRs 0044-0052 merged in, and 0017/0049-0052 accepted
Brings the independent ADR branches (0044-0052) onto one branch so hq lands as a
single MR, and ratifies the five that were still proposed — 0017, and 0049-0052,
which are implemented and green in the lab. With 0053/0054 already accepted here, the
whole ADR chain 0044-0054 is accepted on this branch.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 03:01:06 +02:00
jschoubben b5a74178c3 Merge remote-tracking branches 'origin/design/adr-0044-sdk-boundary', 'origin/design/adr-0045-what-a-module-is', 'origin/design/adr-0046-events', 'origin/design/adr-0047-event-wire-shape', 'origin/worktree-adr-0048-module-broker-account', 'origin/worktree-adr-reachability-dns-firewall', 'origin/worktree-adr-config-is-the-assignments' and 'origin/worktree-adr-module-runtime' into worktree-issue-provider-seal-key 2026-09-05 03:00:24 +02:00
jschoubben 1a83ed28fa Merge pull request '011: measure the graph against every facet a module carries' (#11) from research/011-facet-coverage into main 2026-09-05 02:53:20 +02:00
jschoubben 984194e765 Accept ADR 0054 (slug) and resolve issue 010
ADR 0054 accepted with option E (a declared slug). Issue 010 resolved: the login fits
via the slug, and the minted secret shrinks to 40 chars for S3's secret-key limit —
both halves of an S3 credential now fit the tightest backend.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 02:52:03 +02:00
jschoubben af5c939d16 ADR 0054: add option E (a declared slug) and recommend it over A+B
Implementing A (bound the identity at 20) revealed the readable budget is node+module
<= 14 chars — so tight that the catalogue's own test names (workstation+keycloak, 25)
compact to an opaque hash. B's fallback would fire for the common case, not the rare
overflow, inverting A+B into mostly-opaque identities. Option E — an optional short
slug a module/node declares, preferred over the cleaned name — is the escape hatch B
wanted to be without the opacity: legible because a person chose it, and it makes an
early refusal palatable (refuse on the slug field, not the machine's name). B dropped;
E recommended over a bound of 20, composing with C later if needed.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 02:30:11 +02:00
jschoubben 89b65dd4c0 ADR 0054 (proposed) — a consumer's identity is bounded by the tightest backend
Sketches the options for issue 010: the mesh's identityLimit (63, postgres's) is not
the shortest among the backends the derived name reaches — S3's is 20 — so CheckIdentity
lets an over-long access key through and minio fails at provision time. Options: bound
by the true minimum and refuse at assignment (recommended, with a compact fallback held
in reserve), per-interface bounds, or a provider-generated identity (rejected — breaks
"the mesh says the identity once"). Links issue 010 to it.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 02:17:46 +02:00
jschoubben 75887d0bc5 Issue 010 — the mesh's derived login does not fit every backend's identity rules
Found doing the per-backend provider e2e: redis and postgres accept the mesh's `as`
(mesh_<node>_<module>) verbatim, but minio's S3 access key is capped at 20 chars and
`as` is 22, so the provisioner cannot create the service account. `as` is doing two
jobs — a stable identity the two ends agree on, and a literal identifier a backend
must accept — and those are not always the same string.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 01:58:11 +02:00
jschoubben e5f4af8cf2 Accept ADR 0053 and resolve issue 008 — provider contract implemented and proven
ADR 0053 accepted; adds the scope boundary the umami rework surfaced (credential
provisions vs data provisions — analytics' generated siteId return is left to a
separate decision) and records the lab proof. Issue 008 marked resolved: the sdk
harness and the four adapters are reworked, the symmetric seal removed, and
provider-uses-mesh-credential is green.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 00:27:56 +02:00
jschoubben e27f8b1b7d ADR 0053 (proposed) — a provider creates the credential the mesh minted, and seals nothing
Issue 008's trace confirmed the premise in control-plane code: the mesh already
mints one password per consumer/provider pair and delivers the provider its copy
(SecretFor/SecretsFrom/grantsFor -> Grant.Sealed; the receives contribution carries
As + Secret). The provisioner's symmetric seal is an orphaned, contradictory second
model. ADR 0053 corrects the provider contract in one place (the sdk harness):
providers create the resource with the mesh-supplied login and password and drop
seal/key/return entirely. Reframe 008 as contract-first (every provider, not four).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 00:06:50 +02:00
jschoubben 332c334767 Issue 008 — sharpen: the provisioner's seal model is orphaned, not just undelivered
A cross-repo trace showed nothing writes the provisioner's grant-request files,
nothing reads its sealed credentials, and no consumer unseals — while the mesh
already mints and delivers provider/consumer credentials asymmetrically with no
shared key. The fix is to drop the symmetric seal and have providers consume the
mesh-minted password, a breaking provider-contract change that wants an ADR.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 23:40:20 +02:00
jschoubben 47b0ca5c90 Issue 009 — settings change does not restart a container-hosted runtime
Found rolling the runtime out to the catalogue: a module's runtime reads its
settings-merged config file once at start, but a container is only recreated on a
spec change, and file content is not part of the spec. So updating settings
re-renders the file and nothing re-reads it — ADR 0051's "on the fly" holds only
for config set before first start. Services have restart-on; containers do not.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 23:06:48 +02:00
jschoubben d1aa4254c9 Issue 008 — a provider runtime has no seal key the mesh can deliver
Found building the module-runtime vertical slice: a provider's provisioner
requires a seal key it has no way to receive, and the consumer no way to obtain
the matching one. The runtime cannot come up as delivered.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 22:23:24 +02:00
jschoubben f63eca13b3 ADR 0052 — a module runs its code as its own process, with its own account
The runtime-model gap the review found. A module with tools or events runs one
container — the tool runtime carrying its code — holding the one scoped account
ADR 0048 gave it. A node-wide runtime can't: it would hold the union of every
module's permissions, the isolation 0048 draws. So per-module: one module, one
process, one account. Tools served per key (serve.<tool>) so a caller names a
tool and only its module answers (superseding a shared tools.invoke); events in
the same process under the same account; the runtime image is the tool runtime
plus the module's code (the audit-logger's shape, made the rule). A plain
service module runs no such process. A provider's provisioner is a runtime too —
which is why a provisioner that emits must carry a broker credential or not emit.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 21:24:46 +02:00
jschoubben e898a3ec44 ADR 0051 — a module's configuration is its assignment's, not its manifest
A module is assigned to a node (there is no mesh assignment; 'mesh' is a scope).
The manifest is what the module IS, plus defaults; the configurable values are
settings, carried by the assignment — per-node or mesh-wide, applied at
resolution, changeable live (what a meshboard edits). Extends settings from a
config file's content to the manifest fields marked settable: foremost
listens.from (postgres from:mesh by default, from:anywhere per node — the
firewall follows), and a provider's own config (a registrar's zone/domain/
ingress). Static config in a manifest is config in the wrong place: it cannot
vary per node and cannot change without a rebuild.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 21:05:55 +02:00
jschoubben 9eb5576682 04-ISSUES/003 — fixed: the firewall scope is enforced now
The manifest refuses unknown keys (DisallowUnknownFields), 'from' is the field
that scopes a port and it is rendered to nftables (AsNftables), and the firewall
module applies the rule set. The chain from a declared scope to a dropped packet
is closed. Amended-design: ADR 0050.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 20:56:44 +02:00
jschoubben 5118258ee3 ADR 0049 and 0050 — public DNS and the firewall, two reachability decisions
0049: a public name is provisioned like any capability — a module requires
public-dns and contributes its host; a neutral interface answered by
registrar-scoped providers (cloudflare-dns, route53-dns) that create/remove
the record pointing the name at the mesh's public ingress. Pairs with route
(the proxy) and a public cert (the proxy's ACME).

0050: answers the firewall question. The firewall is NOT a provider like the
proxy — it is a machine's own filter, derived by the host as the sum of what
its modules declare they listen on, with 'from' the whole of public-vs-internal.
Enforced both ways, unknown keys refused — closing 04-ISSUES/003. A public
service is exposed through the proxy (listens from:mesh + requires route), not
by opening its own port.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 20:45:58 +02:00
jschoubben cfa3ad1930 ADR 0048 — ratified: status accepted 2026-09-04 01:22:47 +02:00
jschoubben b622de4fe6 ADR 0048 — a module's broker account is scoped by its emits and consumes
Events (0046) and their wire (0047) left open how a module reaches the
broker. The code has no generic module broker-account: only node and
builder scopes exist, so emits/consumes are enforced by nothing — a
manifest declaring a scope the broker does not draw (04-ISSUES/003).

Decides: on assign, a module gets a broker account whose permissions ARE
the manifest — read on mesh.events + its own queue bound to consumes;
write to mesh.events under module.<self>.* only; nothing else. Consuming
'#' is a deliberate, auditable grant. The account is what makes the
declaration a rule the broker enforces, not a comment.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 01:19:15 +02:00
jschoubben 820ce8fc8c ADR 0047 — the shape of an event on the wire
The wire contract ADR 0046 left open: two topic exchanges (mesh.events,
mesh.rpc, kept apart so # is a clean audit); the routing key as the event
type namespaced by origin (module.*, mesh.*, node.*); metadata in AMQP
headers (required x-event-id/x-source/x-node/x-time/content-type; optional
x-causation-id/x-schema; unknown x- headers ignored) with the body only the
payload; persistent messages; per-consumer durable dead-lettered queues
with prefetch; at-least-once with idempotent consumers (no false exactly-
once). The precedent is ADR 0043 for declarations.

Supersedes the sdk's first cut (metadata in body -> headers); that and the
queue config are code to align in mesh-sdk and mesh-tools.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 23:47:58 +02:00
jschoubben 4bb6dd26ed ADR 0046 — events are a relationship, provisioning's lighter sibling
A module emits and consumes events, both declared (emits/consumes),
parallel to provides/requires. Events are 1:many, broadcast, credential-
free — no provisioner, just the broker's topic routing — so most inter-
module reaction should be an event, not a provision. Every event carries
source/node/time so it is auditable; the audit logger is just a module
consuming '#', no privilege. A consumes for an event nothing emits is a
dangling edge and refused, like requires. One per-node runtime serves
tools, provisioning and events alike.

Extends ADR 0045; builds on ADR 0001 (the broker) and 0044 (emit/on are
stable sdk surface; the binding and runtime are not).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 23:42:25 +02:00
jschoubben b2cb481681 ADR 0045 — what a module is
A module is one self-contained piece of software the mesh installs and
manages; the software is its identity, and capabilities/seats/provisions
are the relationships between modules, not what a module is. Records the
three relationships (shared seat, exclusive seat, provide/require), that
interfaces are mesh-owned and providers adapt to them, and the naming
rule: draw the interface at the consumer's real coupling — neutral where
the coupling is thin (analytics), protocol-scoped where the consumer
speaks a protocol (postgres/mssql/mongodb), never false genericity.

Supersedes 0017 (domain grouping — wrong axis), refines 0002, generalises
0027's protocol-not-product rule.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 22:50:52 +02:00
jschoubben 50d61398b6 ADR 0044 — what the SDK holds, and what it refuses
Supersedes ADR 0030's 'types, not behaviour' line for mesh-sdk. The
boundary is change-frequency, not kind: the SDK holds the stable spine
(tool-serving harness, messaging/event framework, contracts, core
primitives) and refuses per-module clients, per-module tool code, and
anything volatile — because those are what turned hal/sdk into constant
maintenance and made every edit rebuild every module.

States the rule (frequent AND cascading is the disease), why the root
cause was intra-module feature-sharing leaking into inter-module
coupling, and where per-module shared code lives instead (in the
module — a shared file, or a module-local sdk for the few large ones).
Updates repos.md's canonical mesh-sdk description to match; leaves 0030
untouched (immutable).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 21:41:08 +02:00
jschoubben 7b4a664c50 Issues 008 and 009 — what the manifest review could not fix
Both from the 2026-09-02 review of the catalogue examples, and both
design gaps rather than defects in a file: a seed file the host
reconciles back to empty over the grants that grew in it, and a module
stack refused co-assignment because six manifests each own the
directories they exist to share. Fixing either in place would have
been picking an answer the records do not yet hold.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-02 01:07:05 +02:00
jschoubben 9638aa02c5 031 — a machine becomes each thing it was told, in turn
Declarations queue and the host applies all of them, oldest first, so a
machine pushed five things in a minute spends five applies becoming the
last one. Correct every step — each declaration is the whole machine —
and wasted in all but the final step.

Only visible since a report names its declaration: the reports arriving
were about ever-older ones, while timestamp comparisons used to happen
to pass. The fix is consumption order, not the queue; the open question
is what a superseded declaration's report should say, because silence
reads as disobedience and "applied" would be a lie.
2026-09-02 01:06:12 +02:00
jschoubben b245f5e254 011: measure the graph against every facet a module carries
The five declarations cover relations; a module is more than its
relations. Add the facet-by-facet coverage table so the effort cannot
conclude while tools, verification and contributions are unplaced —
and weigh each candidate gap rather than adopting it: contributions
probably dissolve into declared resources, mandatory verifiers risk
trivial ones, and the tool surface is the one facet with no home.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-01 23:14:03 +02:00
jschoubben 6bdd3f2bfa 030 — asking what a machine should be re-signed its certificate
Composing a declaration signed the machine's certificate anew each
time, and a signing carries a fresh random serial — so what the mesh
would send differed from what it had sent by one byte, for ever, and
every machine carrying a certificate stood eternally waiting.

Third find of the same rule: issued once and kept. The port had it, the
secret had it, the certificate composed fresh on every asking — and the
keeping column had existed since the serving-key migration, written by
nothing, the same shape ReleasePorts was found in.

Found by keeping the scenario standing and diffing two plans seconds
apart: one line, where four theories had none.
2026-09-01 22:28:26 +02:00
jschoubben 6385d52bc0 029 fixed — the store is added, not built, and the cycle is refused
The registry module names its image by digest, the way the bundle names
the three a first node starts from, and goes in as a manifest. The lab
now walks that path and passes.

A manifest that provides the store and also builds artifacts is refused
at parse. The lab had been passing only because its Docker Hub stand-in
quietly received the push — a prop covering for the thing under test.
2026-09-01 21:43:42 +02:00
jschoubben 013dbce4a9 029 — the artifact store cannot be delivered by the artifact store
Installing the module that provides `artifact-store` requires something
that provides `artifact-store`: its image is mirrored in, mirroring
publishes to the store, and the builder refuses to run without one.

Never seen, because the lab always has a registry standing before the
mesh asks for one, and so does any mesh built on a machine that already
had one.

What it blocks is larger than a registry. The store holds images and
packed archives both — it is the module catalogue in artefact form — so
until it exists a mesh can run only what its bundle already carries.

The substrate record already answers it. It asks of each candidate
whether it can grant itself the thing it provides: the store cannot
create its own database, the broker cannot create its own virtual host,
and the registry cannot grant itself a repository. So the registry
module names its image and is never built.

With a limit worth saying out loud rather than discovering: a module
providing the store may not build artifacts of its own, a UI or a tool
server included, because there is nowhere to put them until it runs.
Such a registry is two modules.
2026-09-01 21:15:05 +02:00
jschoubben e75683c01f 026 reopened — the rule is right about data, wrong about facilities
Enforcing "declare what you mount" refuses the builder, which mounts the
container runtime's socket. That socket is not the builder's data: it
exists already, the machine owns it, and declaring it as one of the
module's directories would be a lie the host would act on.

Two kinds of mount are spelled identically today — the directory my data
lives in, and a machine facility I was granted. Until a manifest can say
which, the fourteen declared mounts are right by coincidence, which is
what this issue was opened about.

Recorded rather than decided: separating them is new vocabulary, and
inventing it to turn a check green is how a mechanism nobody chose ends
up load-bearing.
2026-09-01 19:45:55 +02:00
jschoubben bd8f09d647 026 fixed — and the coincidence turned into a rule
The fourteen undeclared mounts are declared. More to the point, a
manifest that does not declare one is now refused: they were right by
coincidence, and a checklist nothing enforces is a checklist that is
true until the next commit.

Refused in the control plane, because the machine cannot tell the
difference — asked to mount a path that does not exist, it makes the
directory, which is a thing it is perfectly able to do.
2026-09-01 19:36:15 +02:00
jschoubben 0bcfeb4e80 028 fixed — the mesh assigns the port, and knows what it cannot move
A module now says its port once, in `listens`, and the container's
mapping, the rule set and what a consumer is told are all derived from
one assignment. The three hand-written copies that agreed only because
one person wrote them are gone.

The half that made this an issue rather than an inconvenience was that
the substrate is not a module: nothing in the mesh had heard of its own
store, so it handed a database module the port the store already had.
The machine now says what it carries, and the mesh assigns around it.

Ports the protocol fixes became claims, which needed no new mechanism —
the mesh already had one for what is singular on a machine.

Left open, and unchanged by any of this: whether a module should publish
to the machine at all. Assignment makes publishing safe without making
it necessary.
2026-09-01 18:32:58 +02:00
jschoubben c2a37ab7f8 38 — the mesh assigns the port, and a module does not care
Jochen's call, and the right one: a module cannot choose a port well,
because it is written once and assigned anywhere. Any number it picks is
a guess about a machine it has never seen, and two modules guessing the
same number is not a mistake either of them made.

Writing it up turned up something the issue had missed. The same number
appears three times in every module — the rule set, what a consumer is
told, and what the runtime publishes — and nothing checks that they
agree. They agree today because one person wrote all three. A module
whose `serves` said one thing and whose container published another
would resolve, compose, apply, and hand every consumer a port that
answers nothing.

So the decision is one source with the other two derived, and an
assignment made once and kept, as a credential is.

The part that needed thought is ports that cannot move — mail on 25,
submission on 587. Those become claims, which is what the mesh already
has for what is singular on a machine. Two modules wanting 25 is the
same shape as two wanting the seat, and gets refused by name at
assignment rather than by a container runtime at apply. That makes this
mostly a matter of pointing an existing mechanism at ports.

Left open: whether a module should publish to the machine at all.
Assignment makes publishing safe without making it necessary.
2026-09-01 17:42:27 +02:00
jschoubben 6330abce5f 028 — two things want one port, and nothing says so until the machine
Found by fixing 027 and pushing again. The declaration is now accepted
and the database container still cannot start: the mesh's own store
holds 5432 on that machine, and the module publishes 5432.

Nothing catches it because the substrate is not a module. It arrives
from the bundle before there is a mesh to ask, so the control plane has
never heard of the store and does not know it holds a port. Resolution
can compare modules with each other and cannot compare one against what
the mesh is built on.

Nor does it compare modules with each other. A port is exclusive on a
machine in exactly the way a claim is, and the mesh has a mechanism for
that which ports do not use.

It has been met before: the end-to-end test that exercises a real
database publishes 5433 rather than 5432, inline, with nothing saying
why. That is how a constraint becomes folklore.

The open question is bigger than the bug. Whether a module should
publish to the machine at all decides how a consumer reaches it, and
changes what `serves` means.
2026-09-01 17:21:31 +02:00
jschoubben 03266fd4a2 027 — a container cannot follow a file, and a rotated credential is the case
Found by the forge failing to start. I had put `restart-on` on nine
containers so they would pick up a rotated credential; it belongs to a
service, and the host refused the whole declaration.

Removing it fixes the modules and leaves the reason I reached for it.
The mechanism is written against exactly this, in the host's own words:
a running service does not re-read its configuration, so replace the
file, find it already running, do nothing, and the machine keeps
behaving as before while every check passes. Every word of that applies
to a container, and nearly everything the mesh runs is one.

The cost is concrete. Rotation replaces the file and tells the provider
to accept the new credential. A provider reconciles, so it takes it. A
consumer is usually a container, so it does not — and the two ends hold
different passwords, which is the fault ADR 0001 records costing two
days. The test that proves rotation works uses a consumer that reads the
file on each attempt, so it does not meet this.

Two things a fix has to keep: it stays declared state rather than a
command, because the link may not carry an action; and where an env-file
changed, the honest verb is recreate rather than restart, because a
container's environment is fixed at creation.
2026-09-01 17:19:43 +02:00
jschoubben 3f00d3b413 026 filed; 025 corrected; the image store is a module
Three corrections, two of them to things I wrote today.

026 is the serious one. Four modules mounted fourteen host paths nothing
declared — the mail spool, the databases, the object store's data. The
runtime creates those as root, so owner and mode go unapplied, and the
rule that keeps a directory holding data the mesh did not put there is
written in terms of declared directories. It reached the configuration
and missed the data. The cause was carrying compose files across: a
container shape that can express one gets filled in like one.

025 claimed nothing turns a tag into a digest. That is false, and the
answer was designed and built before I wrote it. A module names an
artifact, not an image, and `kind: upstream` mirrors somebody else's
image into the mesh's own registry, pinned by the digest it lands with.
The two-document split the issue described as the shape of a fix is the
design. Pinning twelve images by hand was treating the symptom, and left
them pointing at a public registry rather than the mesh's.

And the image store was written up as something the mesh does. It is an
ordinary module — considered for the substrate and removed, because the
test is whether the control plane needs it before its first instruction,
not whether it can grant itself one. So somebody's own registry is the
same module as the mesh's.
2026-09-01 16:10:13 +02:00
jschoubben befbb1d978 The five modules could not have run, and now the forge does
Recorded against phase 3, because the phase note said running them
needed images stocked and provisioners built — and missed that not one
of them named an image that exists. Sixty-four zeros where a digest
belongs, eighteen times, parsing and resolving perfectly.

The forge now runs: on a database another module provides, with a
password it did not choose and a connection string it could not have
written itself. First of these descriptions to be started rather than
planned, and it exercises the whole of the credential work.

What remains is the mechanism rather than the data. Nothing turns a tag
into a digest as part of the mesh's own work, so it was done by hand —
which is what the issue says a person should not be asked to do. Asking
a registry takes a second and pulls nothing, so the main argument for
leaving it undone is gone.
2026-09-01 15:40:52 +02:00
jschoubben 2bbdc52440 025 — half done: nothing unrunnable reaches a machine now
The refusal landed, and the examples pin images that exist. What is
still missing is the part that makes it unnecessary: nothing in the mesh
turns a tag into a digest, so it was done by hand — which is precisely
what the issue says a person should not be asked to do.

Recorded because the resolution mechanism turned out to be trivial:
asking a registry what a tag points at takes about a second and pulls
nothing. That removes the main argument for leaving this open.

Also records the two faults that fell out of pinning for real. The mail
system named seven repositories that do not exist, because it publishes
to a different registry than the manifest assumed, and one of the seven
had been renamed upstream. Nothing checking only the shape of a
reference could have found either.
2026-09-01 15:13:52 +02:00
jschoubben fc7717f6b1 021 closed — the record was left open after the fix landed
The code and its tests went in hours before the record was touched, so
an issue that read `located` had been fixed all along. That is the exact
failure the frontmatter exists to prevent: status is meant to be
answerable from the record rather than by reading the code.

Closed with the commit that did it, and cross-referenced to 022 and 023,
which came out of the same mistaken instinct — treating the machine as
a boundary, then as an identity, then finding a consumer had a password
and no name to present with it.
2026-09-01 15:04:36 +02:00
jschoubben 0871e6ec11 37 — where a module lives, proposed
The module descriptions sit in `examples/` inside the control plane, and
that name has been doing harm: everything there reads as a sketch, and
one shipped naming a container image nothing builds. A directory called
the catalogue would have made "does this work" the obvious question.

The shape of the answer turns on one measurement. Of the 126 modules in
the system being replaced, 47 are software in their own right — the
largest is 182 source files, and a speech-capture module carries a whole
daemon. Another 44 ship helper scripts. Only 35 are a description and
nothing else.

So a catalogue cannot be a folder of manifests, because two thirds of
modules are programs. That splits them four ways, and only two of the
four belong in a catalogue: things the world made that we describe, and
packages with some files. What the mesh is made of stays in the
repositories that build it. What we wrote keeps its description beside
its code, in the same commit, because nothing else can stop the two
drifting.

The mesh's list of modules is a table, not a repository, and it already
records where each module came from and at which commit. Nothing needs
inventing for modules from anywhere; a repository of ours is just the
source we curate.

The check that a description is valid should move to a command on the
control plane's binary. Today a test reaches into the control plane's
internals to parse manifests, and another reads its build file to check
images exist — two jobs tangled. A command would also give the same
check to somebody describing their own application, which is the case
that matters most and has none.

Left open: how a provisioner's image gets published and pinned, and
whether thirty-five install-a-package modules deserve to be modules at
all.
2026-09-01 14:24:53 +02:00
jschoubben f6b1834ea9 024 fixed — the registry was addressed by hand and nothing else was
The cause was one line. Machines get a systemd-networkd unit with a
static address, so networkd finishes and reports the link configured.
The registry ran `ip addr add` inline, which leaves networkd waiting to
configure something it was never told about — and
systemd-networkd-wait-online has an infinite timeout.

So network-online.target was never reached and everything ordered after
it never started. On these machines that is Docker, so `docker load`
blocked on a socket whose daemon was queued behind a target that would
never come, and three bounded timeouts stacked to thirty-five minutes.
These machines have no DHCP by design, so that wait was never going to
end.

The hypothesis in this record was wrong and the record now says so.
Stocking had just been changed, so stocking looked guilty; stocking
takes 34 seconds and always did, timed directly before changing
anything.

Fixed with two things that made it cost hours instead of minutes: an
image placement now waits for the runtime and refuses after 120s naming
what systemd is waiting on, and the end-to-end test passes onProgress —
the raise reported every step and the test discarded it, which is why
thirty-five minutes and four minutes of silence looked the same.

The suite then ran to completion, 23 of 24, the one failure a check of
its own flagging a path as a credential because `/` is in the base64
alphabet.

Also recorded: a redirected log lags, because Node block-buffers stdout
to a file. Read as a stall twice, the second time right after the real
fix — where a buffering artifact argues the fix did not work.
2026-09-01 09:53:08 +02:00
jschoubben 407416e6d0 024 — a run stalls before the host is placed, and says nothing while it does
Seen twice today. Once mid-run: thirteen passes, then the process ended
with no summary, no failure and no receipt. Once from the start: the
first test ran 35 minutes against a measured 4.5 and was still running
when it was stopped.

Ruled out rather than assumed: not memory (84 GiB free, no OOM), not the
daemon (the stalled machine answered `incus exec` immediately), and not
the changes under test — the anchor VM had no host log and no
containers, so the run never reached placing the host.

What changed just before is that the rebuild went from two artifacts to
six, and every one of them is pushed into the scenario's registry, which
is the step the second stall sat in. Recorded as what changed, not as
the diagnosis.

The reason this is an issue and not a slow test: the suite prints
nothing between starting a scenario and finishing its first test, so
four minutes and thirty-five look identical from outside, and the only
recourse is to guess. That is how a workstation was left unbootable in
August. And a run that ends silently after thirteen passes is a run
somebody may believe.
2026-09-01 03:20:40 +02:00
jschoubben 39916b26e9 What stands between 3.1 and a running identity provider is a program
023 is fixed, so the design faults are gone and one concrete thing is
left: the realm provisioner does not exist. Its manifest named an image
nothing builds and no program backs, which has been removed — a manifest
describing a program nobody wrote is the same mistake as the credential
files that could never be read.

Keycloak's manifest now says what is true today, and the gap is loud: it
no longer claims to provide oidc-client, so a consumer asking for one is
refused by name at plan time instead of resolving cleanly and waiting
for a client nothing will create.

The provisioner should be written against a real Keycloak in the lab
rather than from the API documentation. The object store's took three
corrections that only a running server produced.
2026-09-01 03:13:11 +02:00
jschoubben 0760280bc6 The largest gap in the coverage list is not a gap
Tool servers — 56 modules, over half — were written up as the biggest
missing thing. They are expressible with what exists, and the first
framing was wrong in a way worth keeping: a module provides `tools` and
the session requires them does not work, because a requirement has one
answer and 56 modules offering tools would be 56 answers.

Turned around it fits exactly. The session provides `tool-host`; every
module offering tools requires it and contributes where its tools are.
Many-to-one is what `contributes` has always been, and the session
receives all of them in one file. Verified by resolving it rather than
by reading the code.

It only became possible today: until 022, several modules on one node
requiring the same thing was refused outright. Worth noting because it
means the credential fix bought more than credentials.

What remains is a decision about what a tool server is, which is work
rather than a missing shape.

The entry stays in the list rather than being deleted — a checklist that
quietly loses its biggest item reads as though nobody looked.
2026-09-01 03:09:11 +02:00
jschoubben f583502fc9 023 fixed — the mesh says who a consumer is, and what it is bound to
Both halves had one cause: the mesh knew something and did not say it.

Who a consumer is now comes from one derivation, sent to the provider in
its grant and to the consumer in its binding, so the two agree by
construction. The provisioners use the name they are given and refuse to
invent one, because a name of their own would create a login the
consumer could never guess while everything reported success.

Bound values reach the file that needs them through the symmetric twin
of the sealed placeholder — simpler, because they are not secret, so the
control plane fills them in and the host gains nothing.

The lab run meant to prove this failed in a way that looked like the fix
being wrong: rotation could not authenticate against a real database.
The cause was the suite rebuilding the control plane's image and not the
provisioner's, so an image built that minute ran against a provisioner
built the day before. That is 005's family and is recorded with the
issue, because the misleading part is worth more than the fix.
2026-09-01 03:05:43 +02:00
jschoubben 4adc656b54 Where Phase 3 actually stands
All three modules have manifests, all three parse, resolve and plan, and
none of them can start. Worth writing down before it reads as progress
or as failure, because it is neither.

The vocabulary held. Nothing in 3.1–3.3 needed a new shape — including
the mail system's several containers on a private network, which was the
one expected to break it. That was the question this phase was designed
to answer.

What did not hold was underneath: 022, now fixed, and 023, open. Both
are about credentials rather than about what a module can say.

The third fault was in the manifests, not the design: a secret declared
at a path named .env and read as one, when a sealed file holds a
password and nothing else. That is what a manifest checked only by a
parser buys, and it is why there are now two tests reading the manifests
on disk.

023 is the whole of what remains before the identity provider runs.
2026-09-01 02:56:54 +02:00
jschoubben dea46c3c6c Correct what the coverage document said about actions
I wrote that a module cannot declare an action. It could — the parser
accepted one, and the refusal only came on the machine. The claim was
wrong in the direction that matters: it read as "the design prevents
this", when what prevented it was a check at the far end that nobody
would connect back to the manifest.

Health checks are still the gap most worth closing, but the shape of the
answer is different from what I wrote. An action is not available to a
module at all, so a health check needs a way to say ask this and expect
that without saying run this — closer to a listens entry than to an
action.

Also records the finding itself, because it is a recurring shape here
and not a one-off: a rule enforced only at the far end is enforced and
unusable.
2026-09-01 02:56:05 +02:00
jschoubben 9073d3f2df Say plainly that env-file never points at a sealed secret
The playbook offered `env-file` and `${secret:name}` as alternatives,
and that reading is what produced the bug every example module shipped
with: own-secrets pointing at a path named `.env`, mounted as env-file,
holding a bare password. The container starts with no password set —
which is a service running on the wrong credential, not a failure.

They are not alternatives. A sealed file holds a password and nothing
else, so env-file points at a file the module declares whose content
leaves a hole, and the host fills it on the machine. A provisioner is
the exception, because it reads a password file.

Written out as the three lines a module needs, with the failure it
prevents named, since the abstract version was already there and was
read the other way.
2026-09-01 02:52:55 +02:00
jschoubben 2baf22ac43 A pair is a module and a provider, not two machines
Amends the credentials page, which said "every pair has its own
credential" and meant two machines. Built that way, it was wrong in a
way that only shows on a real node: a machine running several services
against one database server had one credential between them, so the
provider refused to plan at all and the consuming node quietly gave the
first module a credential and the rest nothing.

The page already argues the case against itself — one credential with
many holders is the first of the three faults it was written to remove.
It just drew the boundary at the machine.

Two modules on one node are as separate as two on different nodes, and
one login opening both is what this page exists to prevent. It is also
what makes withdrawal possible: one role per machine cannot say that
this module has lost its login and the others still have theirs.
2026-09-01 02:46:37 +02:00
jschoubben 80f18caf03 022 fixed; 023 filed — a password is not a connection
022 turned out to have a silent half worth recording: the provider
refuses loudly and names the modules, which reads as a decision, while
the consuming node does not refuse at all. Three modules wanting one
database produce one need, so two of them get no credential file and
each starts and fails to authenticate with nothing saying why.

023 is what remained after fixing it. A consumer now gets its own
password, in whatever shape its configuration wants, and still cannot
connect: the user name is invented by the provisioner and recorded
nowhere in the mesh, and the host and port sit in a JSON binding that an
application reading KEY=value cannot use.

The asymmetry is backwards and the coverage document now says so. The
secret is the hard case, because the mesh must not be able to read it,
and the secret is the part that arrives. The host and port are ordinary
facts the mesh holds in the clear, and they are the ones stuck.

Keycloak, Gitea, Mailu and MinIO all parse and resolve and none of them
can start. This is what stands between the module set and a running one.
2026-09-01 02:40:40 +02:00
jschoubben c86adbe3cc 022 — a credential belongs to a node, so a second consumer refuses
Found while checking whether the module vocabulary covers real use
cases. A node running three modules that all want a database cannot be
planned at all:

  anchor has 3 modules asking for "postgres-database" and they would
  share one credential: gitea, keycloak, umami

The refusal is right about what it says and wrong about what it implies.
They would share one credential, and sharing is worse than refusing —
but the arrangement being refused is the ordinary one, and the node this
mesh exists to take over runs eight modules against one database server.

The cause is the key: a credential is keyed by provision, consumer node
and provider node, so `consumer` is a machine. The provisioner inherits
it and names the role `mesh_<node>`. The refusal is not a check that
caught something; it is the only honest thing that function can do with
a key that cannot tell two consumers apart.

It is the same mistake as 021 with a different face. There the machine
was treated as a trust boundary; here it is treated as an identity, as
though "who is asking" is answered by naming a host. Two modules on one
node are as separate as two on different nodes.

Worth stating plainly: without the refusal, gitea's login would have
opened keycloak's database, and nothing would have said so — from the
provisioner's side it created exactly what it was asked to create.

Not a local fix. It crosses the control plane, the grant file naming and
every provisioner that names something after a consumer.
2026-09-01 02:28:39 +02:00
jschoubben 36d342f176 What a module must be able to say, measured against 127 that exist
Every manifest in the system being replaced was read and every key
counted, then set against what the new one can express. Three findings
worth more than the table.

**The most-used key was already covered and I expected a gap.**
Depending on another module — 65 manifests, the commonest thing any of
them says — is a requirement naming a module, which already means that
module rather than anything providing the name.

**The largest real gap is tool servers: 56 modules, over half.** A
module can already run one; what is missing is anything saying it offers
tools. That is plausibly a provision rather than new vocabulary, which
would need nothing added — not yet decided, and recorded as undecided.

**The gap most worth closing is health, at seven modules.** The mesh
knows a container is running, which is not whether it answers, and this
project has paid for that distinction twice. An action with a verify is
exactly the right shape and may not arrive over the link, so a module
cannot declare one.

Two things are missing deliberately and say so: stage hooks, because the
link may not carry an action and a module needing setup ships a program;
and flavours, retired in favour of claims.

Config merging is missing and should stay missing. A mechanism that
understands TOML gets asked for YAML, then INI, which is how the thing
being replaced became unholdable.

Also records what the survey found that is not about coverage: manifests
that had stopped matching what was actually brokered, one fact derived
in two places giving two answers, and a live listing returning
credentials in plaintext.
2026-09-01 02:20:34 +02:00
jschoubben 1ee62392b9 Playbook 06 — writing a module, from doing it once
Written after porting the first real workload end to end. Every step
exists because skipping it cost something, and the ratio is recorded
because it is the lesson: six attempts, one real bug, and the mesh was
right every time.

The rule worth carrying out of it: read the host's log before
theorising. A declaration that was sent and not applied says so there
and nowhere else — it took an hour to look, and the answer was one line.
2026-09-01 01:46:46 +02:00
jschoubben ecfc2c215e Point the plan at the survey, and name the likelier failure
The conversion's detail is operational and names machines, so it lives
in the mesh's knowledge base rather than in this repository:
`migration/where-service-data-lives` for where every service's data
actually sits, and `troubleshooting/db-password-frozen-at-first-init`
for the lockout. This document says the rule; those say the specifics.

The lockout is the finding worth carrying here, because it is worse than
the one this plan was already guarding against and it is likelier. A
database image consumes its password variable only when its data
directory is empty. Everything keeps data on a persistent directory, so
the role holds whatever password it was created with for ever;
regenerate the variable and the application moves on while the database
does not, permanently, because nothing reconciles it.

Eight modules are in that state today and work only because nobody has
regenerated their credential since their data directory was created.
It was already documented in the knowledge base and my survey had missed
it — found by searching, which is the argument for the knowledge base
existing.
2026-08-31 21:46:53 +02:00
jschoubben 122405df8e 020: the server version is not it either
Pinned 2.5.0 rather than latest, on the suspicion that its draft
profiles extension was involved. Identical failure, so that is ruled out
and recorded — two of the three guesses in this issue have now been
tested and both were wrong, which is the useful half.

The scenario keeps the pin regardless; it should have had one from the
start.
2026-08-31 21:42:13 +02:00
jschoubben acd5a0d80b File 020 — a certificate is issued and never collected; close Phase 1
Against a real ACME server the proxy orders, the challenge is answered
at the name on port 80 through the proxy itself, the authorisation goes
valid, finalisation is accepted, and the authority issues a certificate.
The client then posts to an empty URL to collect it, and never does.

Read from the authority's own log rather than inferred. Across one run
it issued two certificates and accepted finalise three times: the client
reaches issuance every attempt and fails at the same step after it.

Ruled out and recorded, so nobody repeats it: the directory is complete;
the authority's API certificate covers the address; the challenge path
works. A hand-written server config was suspected and was wrong —
replacing it with the server's own default, changing only the challenge
port, gives the identical error.

Filed rather than pursued because what remains is interop between two
libraries against a server that exists to be a test server, and may say
nothing about a real authority. What the mesh needed to show, it showed:
a routed name gets a certificate ordered from a configured authority,
and an unrouted one gets nothing — that second assertion passes.

Phase 1 closes with this one item partly open. Two of its four tasks
needed no code at all, the network shape was built, and the next thing
to learn comes from moving a module rather than a fourth lab run.
2026-08-31 21:40:03 +02:00
jschoubben f4347e2f14 Correct an overstatement: a sealed secret is readable on its node
An earlier paragraph implied a secret becomes unrecoverable once
accepted. It does not. It is sealed to the node, which holds the private
half and writes the plaintext into the module's own file at 0600 — the
value is there, on the machine, as an ordinary file.

What does not exist is a way to ask the mesh what a secret is. That is
the property worth having and it is narrower than what was written.

The reason to capture the old system's environment first is simply that
adoption means supplying those values, not that they become
unrecoverable.
2026-08-31 21:34:50 +02:00
jschoubben b68103d198 A module is adopted with the credentials it already has
Nothing is rotated during the conversion. A service keeps the password
it is already using, because minting a new one is how a running service
stops being able to reach its own database mid-migration.

The mesh has both paths already: generate-and-seal for a new module,
accept-and-seal for an adopted one. Adoption needs the second, and it is
built.

Rotation becomes a separate act afterwards, once everything works — the
machinery is proven, and it is a thing to do deliberately rather than as
a side effect of moving a service between systems.

Records the step that has to come first and is easy to miss: read the
current environment out of the old system while it can still be read.
Once accepted, the mesh cannot show a secret back, and once the old
system is gone neither can that. A password nobody wrote down is a
service nobody can adopt.
2026-08-31 21:31:26 +02:00
jschoubben f801b4b3e2 The board is published publicly, which decides everything else about it
Assumed throughout and stated nowhere — the wrong way round for the most
consequential fact about this component.

A board reachable only over the private network would sit inside the
boundary 0004 already calls the security boundary, and a login there
would guard a room whose door is inside the building. This one faces the
internet, so its login is a perimeter rather than defence in depth.

Which makes the identity provider the mesh's outermost gate. The board
presents the control plane, and the control plane's networked surfaces
can change the mesh (0035) — so whoever that provider admits can assign
modules, from anywhere. Written flatly because it is easy to arrive at
one reasonable step at a time and then be surprised by.

What follows is not the board's own design: who may log in is a decision
about the mesh rather than about an application; a public name needs a
certificate from an authority the world trusts, which is why that work
exists; and the provider going wrong in the permissive direction is a
mesh-wide exposure with no local symptom.

The command line is unaffected and is why this is tolerable — it
authenticates through nothing and answers to the machine's own login, so
the mesh stays operable by somebody standing at it whatever happens to
the gate. That is the property to protect if the rest is ever traded
away.
2026-08-31 21:26:06 +02:00
jschoubben a1a10e9ed2 Bootstrap ends at a usable mesh, and the first credential comes from a person
Bootstrap stopped when the control plane started — a mesh that runs and
cannot be used by anybody not standing at the machine, since the
networked surfaces need an identity provider and no module has been
assigned yet. It now runs through the provider and the first login.

The obstacle was not incidental. The mesh has never held a readable
secret: Make generates and seals, keeping no readable copy. An initial
administrator's credential is the first value a person must read.

Generating it and printing it once was the convenient option and is
refused. It would give the control plane a plaintext secret for the
first time — briefly, and to one terminal, but the capability would then
exist, and an exception made for one case does not stay one. The next
awkward credential gets printed too, and "a copy of the database is a
copy of nothing" stops being checkable by reading the code.

So the operator supplies it, on standard input, not echoed — the path
that already exists for a model-access key. What is created is an
account in the identity provider, not a user of the mesh; there is still
no user model.

Unattended bootstrap remains possible and the value still comes from
outside: automation supplying it is the operator supplying it. What is
refused is the mesh inventing one, so an unattended bootstrap with
nothing provided yields a mesh with no administrator — correct rather
than broken.
2026-08-31 21:23:55 +02:00
jschoubben 0ef6d3f574 One implementation, several surfaces, and what that costs
The mesh is operated from a command line and must be operable from a
browser and from a model's tools, without becoming three systems. The
pattern is already in the code and was unnamed: `board` serves HTTP by
calling the same functions the CLI calls, holding nothing.

Takes the decision 0034 said had to be taken deliberately rather than
arrive with a feature: the HTTP surface is not read-only, so a browser
login now carries authority over the mesh.

Names the dependency by protocol — an OAuth2 identity provider — as the
mesh does for AMQP, S3 and OCI. Keycloak is what fills the role; what
the control plane knows is that it validates a token, and replacing the
provider is a migration rather than a redesign.

Says what this must not become, because it is the failure the project
was started over: a kernel every module imports, 155 files of code from
every context. Shared surfaces are not a shared library. Three adapters
calling the same functions is not the same as logic leaving the context
that owns it.

And records the loop it creates. The networked surfaces depend on a
module the control plane assigns, so when identity is down nobody can
authenticate — including whoever is trying to fix it. The way out is the
command line, which authenticates through nothing and is available to
the account that owns the machine. Hence the rule: no capability exists
only behind an authenticated surface, because that is a capability which
disappears exactly when identity does.
2026-08-31 21:17:17 +02:00
jschoubben fccac61e58 The board is a web application, not a category
Supersedes 0032, which decided the right thing and described it wrongly.
The decision is unchanged: the account that installed the host owns the
mesh, and there is no user model.

What was wrong was inventing "a surface that delegates authentication"
for the board. It is a web application with a login, in the way every
web application has a login. That is a fact about an application, not a
property of the mesh.

The cost was not cosmetic. It made the identity module look like part of
the mesh's authority — something the mesh depends on to know who anybody
is — when the mesh knows nothing about people at all and one of the
applications running on it happens to have a login.

Keeps the line that is worth writing down, and states it more plainly:
signing in to an application must not become authority over the mesh.
Today it cannot, because the board reads and does not act. The moment it
can assign a module, whoever it lets in has mesh authority — and it
would arrive as a feature rather than as a decision. So a surface that
can change the mesh is a change to who owns the mesh, and is taken as
one. Not forbidden; just not something that turns up in a pull request
titled "add assign button".
2026-08-31 21:08:39 +02:00
jschoubben 10fa7c76d7 The substrate is a store and a broker
Third correction to one table today, found the same way as the other
two: by asking whether both halves of the test were answered, or only
the easy one.

0006 admits the registry because "it cannot grant itself a repository" —
true, and the second half. Nothing established that the control plane
needs one in order to run. Counted rather than argued: the bundle raises
twelve resources and no registry is among them. The registry arrives
afterwards as an ordinary module, which is exactly what the lab asserts.

0006 half-said this already, calling it "substrate by role and ordinary
by delivery, provisioned once there is a control plane to do it". A
member provisioned by the thing it supposedly precedes is not a member;
that phrase was carrying a contradiction rather than resolving one.

The registry is a closer call than the object store and the difference
is worth keeping: the control plane never touches an object store at
all, but it genuinely uses the registry. So the registry is a real
dependency of the mesh operating and not of the control plane starting —
and it is the second that the word means.

The substrate is now exactly what the bundle raises, which is the
strongest form the list can take: checkable by counting rather than by
reading an argument, and the two cannot drift.

The finding is not about substrates. A test with two conditions is a
test only when both are asked.
2026-08-31 20:30:19 +02:00
jschoubben a028337490 The local account owns the mesh; a surface delegates to a module
Answers what 0031 left open, and a question it did not ask — who owns
the mesh at all. There was no answer, and the absence was invisible
because every operation so far has been run by the person sitting at the
machine, so nothing had to say whether that was the design or the
circumstance.

The account that installed the host owns the mesh on that node. No user
model, no roles, nothing to administer. It follows from 0004 rather than
adding to it: there is no authorisation between nodes because every node
is the operator's own, so a user model inside that boundary would guard
nothing — anyone it could stop could read the node's key off the disk.

The board is different, and the difference is the network. A surface
reachable by a browser has to know who is asking, because those people
are not by construction people with a shell on the machine. So it
delegates to an OAuth provider, which is a module.

That does not make identity substrate. A surface delegating
authentication is not the control plane delegating it: the control plane
runs, applies declarations and reaches nodes with no identity provider
in existence. Only the board needs one.

Records the cost plainly: anybody with a shell on a node has full
authority there, and there is no way to give somebody authority over one
node without giving them a login on it.
2026-08-31 20:23:42 +02:00
jschoubben e3934e4449 The control plane authenticates nobody, so identity is a module
Closes the last open question about what the substrate contains. 0006
left an identity provider conditional — substrate only if the control
plane delegated authentication — and said the decision had not been
taken. It is now: it delegates to nothing.

The conditional was never about machines. A node proves itself with a
keypair it generated over a broker account issued at enrolment, and
declarations are verified by signature; none of that involves an
identity provider. It was only ever about whether a person signing in to
a mesh surface would be authenticated by something else.

So the substrate is three — a relational store, a message bus, an image
registry — and with 0028 having removed the object store, no member is
conditional and every one is there for the same reason.

It does not settle how a person signs in to a surface, deliberately.
What is settled is that whatever answers that is not something which
must exist before the mesh does, so it can be decided late or replaced —
which being substrate would have prevented.
2026-08-31 20:18:03 +02:00
jschoubben c570c687f6 The handover: switch off the old brain, leave the services running
The conversion method, recorded because it decides everything else and
was not written down.

The old control plane is stopped — provisioning, coordinator, syncs, the
pipeline, anything that decides or writes. The workloads it was managing
keep running, because nothing is managing them. The new mesh then takes
ownership one module at a time.

Nothing is ever unassigned in the old system. Unassigning is how it
removes things and removing is how data is lost; it is asked to stop
having opinions, never to take anything away.

Disabled rather than merely stopped, which is the part easy to get
wrong: those units are enabled, so a stop lasts until the next reboot. A
reboot mid-conversion would bring the old control plane back to
regenerate managed files underneath the new one — the one situation
where two systems really would fight over a machine.

A service left running with nothing managing it is the safe state: it
has its data, its configuration is on disk, and nothing will change
either. The risk in a conversion is in the managing, not the running.

Also records why taking ownership piecemeal is safe: the new host's
orphan removal is per-origin, so it only removes what it recorded
itself. Services it was never told about are not orphans to it.
2026-08-31 19:55:35 +02:00
jschoubben d1ab2dc0b4 Data outlives the mesh that declared it, and the conversion starts where it lives
0030, found by asking what the conversion actually needs rather than by
reviewing anything. The host deleted a directory and everything under it
when it stopped being declared — which happens when a module is
unassigned, or when a manifest is edited to move a data folder, which is
the exact operation this plan needs. A database's files, a mail spool.
The report said "removed".

A directory still holding something is now kept and said so. No flag and
nothing to remember: emptiness is the test, and it works because the
removal order was already right — the mesh's own contents are gone by
the time the directory is reached, so what remains is by definition
something nobody declared.

The plan now says data outranks its own ordering: copy, read back
through the service that owns it, and only then point anything at the
new location. Never move and then check.

And it records where this starts — the node holding all the production
data — with what that costs stated rather than argued with. Everything
proven so far was proven on machines that could be destroyed and raised
again. A scenario proves the mechanism, not the state on that machine.
2026-08-31 19:54:41 +02:00
jschoubben 9ad0ec35e0 The conversion is done by hand, and that removes work from this plan
Recorded because it is load-bearing and was not written down: moving
from the current system to this one is a person at a command line, not a
migration program.

What that removes is larger than what it adds. Nothing in this plan
needs an importer, a translation layer, a compatibility shim, or a way
of keeping two systems agreeing while both are live — each of which
somebody would otherwise reasonably build, use once, and maintain for a
year.

It also settles what "safe" means for the system being retired: a fix to
it must be safe on its own, because there is no careful rollout to
sequence it into. A change needing three steps in the right order is a
change that will be half-applied. That reversed a certificate default I
had chosen this morning.
2026-08-31 19:46:21 +02:00
jschoubben e4327a3a5e Phase 1.3 done: ordering was already there, the network was not
Ordering needed no change for the third time running — resources apply
in the order declared and nothing sorts them — and is now asserted,
because sorting them for any sensible reason would have passed every
other test.

Separates ordering from readiness, which the task had run together: a
container started is not a container ready. Nothing waits, and what
needs something usable retries. That is deliberate and more robust than
start ordering, since a dependency can restart long after apply.

The network was the first thing in Phase 1 that genuinely needed
building, and the first that needed a decision: 0029 records why a shape
rather than an action, and the vocabulary is nine.
2026-08-31 18:55:22 +02:00
jschoubben ce486fd5d2 Phase 1.2 done, and it is the same surprise as 1.1
A session as a licence consumer needed no change either: the two
sessions are two modules, so the existing (node, module) binding already
names them apart. 14-model-access.md's "a step toward it and not it" is
true of a worker and not of a session, and the difference is that there
is one session per node rather than many per machine.

Records what stays open: the worker half of that gap is real and
unaffected, and belongs with 0003, which is unbuilt.

Two tasks in a row that were already possible. Both were written from
the design rather than from the code — the review's own finding arriving
in the plan it produced. The remaining Phase 1 items should be checked
against the code before being started rather than after.
2026-08-31 18:41:19 +02:00
jschoubben fdd909ec40 Phase 1.1 done, and it was not the task that was written down
An object-store provision, proven against a real store with seven
assertions.

The finding is worth more than the task: the control plane
special-cases nothing. provides, requires, contributes and grants are
name-agnostic, so asking for a bucket needed no change to the mesh at
all. What was missing was a provider and the last step on the machine —
"add an object-store provision" was never mesh work, and the breakdown
now says so rather than leaving the next person to rediscover it.

Named s3-bucket by 0027: the coupling is to the API, not the product,
because swapping one store for another does not break a consumer. A
database is the other case and names its engine.

Records the assertion a database does not need, because it is the one
that will be forgotten when somebody writes the next provider: one store
holds every bucket behind one endpoint, so isolation is a policy rather
than a property, and a policy granting everything passes every test that
only checks a consumer can reach its own bucket.
2026-08-31 17:53:02 +02:00
jschoubben cb1954e7b5 Accept 0024, and rewrite the work breakdown around what is actually being done
**0024 accepted.** Model access was decided, built, and proven in the
lab, and two design documents rest on it; only the status had never
moved. The gate is green again.

**The work breakdown rewritten.** It planned a decomposition of the
existing system in place — extract contexts, declared features, shrink
the shared library. That is not the work. A replacement is being built
beside it, and only the old Phase 0 survived contact with reality, so
the one document meant to say what happens next was describing a system
being retired.

Now ordered by what "modules move across one at a time until the old
registry is off" actually requires:

- Phase 0 is marked done against the twenty-two lab assertions, **and
  carries its own limitation**: every module exercised was written to
  test the mechanism, so the vocabulary was shaped by its own fixtures.
- Phase 1 is the vocabulary gaps found by asking what real modules
  need — an object-store provision, a session as a licence consumer, a
  network shape with ordering, public certificate issuance.
- Phase 2 is one module, then a week of running it, because the point of
  going first is to find what Phase 1 missed.
- Phase 3 picks modules that each prove something the first did not; the
  mail system is last because it is the one that may send work back into
  the declaration language.
- Phase 4 is switching the registry off, named as a phase so it is not
  mistaken for the goal.

Keeps the rules of engagement unchanged — they were about how work is
done, not what it is — with one addition: stop and ask before anything
that touches a machine outside the lab.

Adds a section on keeping the list true, since the document it replaces
was wrong for weeks and nothing said so. A claim here is counted, not
reasoned, and a phase is done when the lab says so.
2026-08-31 17:25:27 +02:00
jschoubben 1b5308c9cc Review of the to-be layer: check what the documents claim against what runs
First pass of a design review, done by reading documents against code
and against a raised mesh rather than against each other. Every error
below was invisible to a proofread.

**Statuses were stale, and nothing checked them.** Ten to-be documents
said `designed` while naming working, lab-proven code — several with a
*What was built* or *Raised, and observed* section. Added a
`status-vs-code` check: naming a file is a claim that the file
implements this, so a document that points at one has stopped being
merely designed. It failed on all ten before it passed, per the rule
this folder sets for its own checks.

**The bundle carries three images, not two.** 07 reasoned about which
substrate services go in and overlooked that the control plane is in
there too — it is what the substrate exists to start, and there is
nothing to fetch it with yet. Counted, not deduced.

**The bootstrap uses four shapes, not six.** It listed `file` and
`directory`, which substrate-first-node.lock never asks for. The claim
that mattered — nothing is blocked on the host — was true either way,
which is why the wrong count survived.

**The eight capabilities were documented nowhere.** Implemented in
internal/profile/detectors.go and enumerated in no document, including
the one about the host that detects them. A vocabulary modules write
against, readable only by reading the code. Now written down, with the
seat/graphical-session distinction that is wrong in both directions if
collapsed.

**MinIO swept out of the to-be layer** per 0028.

The gate now fails on one thing left deliberately: ADR 0024 is
`proposed` while two documents rest on it and the feature it decides is
built and lab-proven. Accepting a decision is not mine to do.
2026-08-31 17:20:47 +02:00
jschoubben cbcbba8099 A provision names its engine; the substrate supplies only the control plane
**0027 — provisions.** A module written against PostgreSQL could be
matched to a provider of SQL Server, resolve as satisfied, and fail on
its first query. The name said the role, so nothing distinguished
engines. Refusing on ambiguity could not help: with one provider of
each name nothing is ambiguous. Enforced at parse rather than
documented, because the old naming was the documentation.

**0028 — the substrate.** 0006 admits an object store on the grounds
that it cannot grant itself a bucket. That answers the second half of
the test and assumes the first: the control plane does not need one.
Verified — no S3 client in mesh-control, and internal/builder/registry.go
records the deliberate choice to put artifacts in the OCI registry as
content-addressed blobs. The row was inherited from the system being
replaced, where an object store distributed module tarballs, and was
never re-tested against the definition above it.

So an object store is an ordinary module, and a mesh with nothing
needing one runs none. Migrating it is module work, not substrate work.

0028 also states what 0006 left unsaid: a substrate service and a
module of the same product are different instances. The substrate is
raised from the bundle before any mesh exists, so it is not in the
module graph — a workload depending on it would depend on something the
graph cannot see, cannot rotate a credential for, and cannot move, and
would put workload data in the store the control plane keeps its own
state in.

Both records were found by reading code against design rather than
design against itself, which is the review that should have happened
sooner.
2026-08-31 17:13:07 +02:00
jschoubben 046a990198 Several sessions at once is a surface property, not a session one
Answers the question 15 raised: a board showing many sessions leaves
one-per-node untouched, because each is still one conversation. Only
concurrent conversations with the same session would touch 0004.

Records soulstream and herdr as the prior art to draw from, and marks
it explicitly off the provisioning path so it stays a note rather than
becoming the work.
2026-08-31 16:52:15 +02:00
jschoubben fbf5b1d04b A session's memory is its own, and it is not declared
Settles the question 15 left open: the mesh session holds its own
memory in the mesh root, rather than assembling a view over the node
sessions. Memory follows the rule the rest of the design already uses —
the context root is the whole of what makes one session a different
agent, and memory is part of what makes it that agent.

The control-plane node is what makes this load-bearing rather than
tidy. Two sessions share that machine; if memory belonged to the
machine instead of the root they would share it too, and the mesh's
recollection would be indistinguishable from that node's own — the
collision 0026 exists to avoid, arriving through the back door.

Also corrects an error made writing it up: memory is NOT declared
state. The engram and tools are — the mesh says what they are and the
host writes them (0011). Memory is written by the session itself and
declared by nobody, so a mechanism that regenerates the root wholesale
would erase it on the next heartbeat, silently, while reporting
success. The root is not uniformly managed and which parts are has to
be explicit.
2026-08-31 16:41:06 +02:00
jschoubben 3c6c16abdf The mesh has a session of its own, and it is the node session's mechanism
A session for the mesh itself, addressed as the mesh, differing from a
node's in exactly three things: the context it starts in, its engram,
and its licence binding. Not a new kind of agent — the same mechanism
pointed at a different root. Two implementations of one mechanism drift,
and the vocabulary collision 0001 exists to undo began exactly that way.

It runs on the control-plane node, and the reasoning is easy to get
backwards: not "the important agent on the important machine", but that
this node is already the one place excepted from "compromise of a node
is compromise of that node". Placed anywhere else it would create a
second such place.

It is an addition to per-node messaging and never a replacement. 0001
holds that losing the control plane costs change, not operation — and a
mesh whose only conversational surface lived there would lose the
ability to ask anything while every machine kept running perfectly.

Writing it up exposed that the node session's setup was never designed
at all. 0004 gives behaviour and stops: nothing said how a session
starts, where its context lives, or how a broker message becomes a
prompt. That gap was invisible until something had to be built *like* a
node session. 15-the-agent-session.md covers both as one mechanism.

It also makes "a consumer that is not a machine" undeferrable. The
control-plane node now hosts two sessions that must hold different
licences, and a per-machine binding cannot express that at all. Noted in
14-model-access.md against the gap it was already recorded as.

Also completes the to-be index, which stopped at 10 and omitted four
documents. Pre-existing broken ADR references in the older rows are left
alone rather than guessed at.
2026-08-31 16:32:59 +02:00
jschoubben e823cc1cc5 The design record is read where it is written, never copied to be found
Decides the question 006 narrowed to. An agent reads this repository
directly and the search consults it, so these documents surface beside
ordinary results instead of only when somebody already suspects they
exist.

A scheduled sync into the mesh's memory was the option that works with
what exists today, and lost on the ground this repository can least
afford: it makes a second copy, and the copy that is searched quietly
stops matching the copy that is edited. A design record that has
silently diverged from the reasoning it claims to carry is worse than
one that cannot be found — the first misleads, the second merely fails.

Amends what 0019 promised rather than satisfying it: these documents
will not be indexed, they will be read. The commitment that survives is
the one that mattered — that a searcher finds them without already
suspecting they exist.

Gated on an agent that does not exist yet, so 006 stays open on the
build with a decided shape. What closes it is a check that fails today
by design: search the mesh's memory for a phrase that appears only in a
design document here, and require it back.
2026-08-31 15:45:00 +02:00
jschoubben c192810fba 004 and 008 resolved; 006 narrowed to the decision it actually needs
**004 — certificate issuance.** The resolver declared no authority at
all, so the client fell to its built-in production default: there was no
setting set wrongly, there was no setting. It is now a node property
defaulting to staging, which answers the first open question. Staging by
default rather than production-with-an-override, because the alternative
leaves the safe path depending on remembering to opt out of it — 005's
lesson, in a second place. The rollout is ordered and the order is the
dangerous part; recorded, not performed.

**008 — node rescue.** Read back from running nodes as the report asked,
and one of its own claims was wrong in a way that matters: the health
timer does exist and does fire. It simply never calls the rescue script.
A trigger that exists and does not do what the script claims survives a
halfway check, which makes it worse than the absence the report
described. Resolved by making the documentation true, not by
implementing rescue — the replacement host already supervises recovery,
and wiring unattended restart into the fleet being retired is a
deliberate decision rather than a tidy-up. Two "self-healing" claims
narrowed to what they actually do.

**006 — deliberately not closed.** Re-checked today: the indexing still
does not exist. What is gone is the reason it was an issue — the claim
is no longer load-bearing, because the README names the gap and the
decision's reasoning never invoked indexing. A signpost now points here
from the knowledge base, and was measured rather than assumed: it is
reachable, it is not surfacing. Closing it while the indexing does not
exist would be this repository's own named failure, one folder from
where it names it.
2026-08-31 15:20:11 +02:00
jschoubben 345bbe0552 005 resolved: a suite that cannot run on every push says when it last ran
Retired in favour of the lab rather than repaired — that answers the
first open question. The second finding is the one that generalises:
"nothing runs it, and nothing reports that nothing runs it" is not a
fact about that harness, it is a fact about any suite too expensive to
run on every push. The replacement inherited the fault it was replacing.

Records the three rules that now hold, and what the fix taught twice:
the remedy rebuilt the symptom inside itself, and the code that counts
results passed every test while reading nothing.
2026-08-31 15:02:26 +02:00
jschoubben f3ffdae909 Record the resolver as built, and the two things it must not do
A service is reached at <service>.<node>.internal, so what resolves is anything
under a node's name. The mesh writes the data and runs no daemon; two roles,
two claims, because systemd-resolved cannot serve a wildcard at all.

Both prohibitions were found by a machine rather than by reasoning: an address
systemd already held, and reading resolv.conf for upstreams that now point at
itself.
2026-08-31 14:23:02 +02:00
jschoubben 6ecd03694b Issue 019: a comment asserting a fact about a machine, which nothing checked
Twice in one file, a statement about a machine that read as reasoned and was
wrong — and the module's unit tests all passed while the daemon could not
start. That is what a unit test is: it confirms the assertion was made, never
that it is true of any machine.

003 in prose rather than in a manifest key.
2026-08-31 14:11:34 +02:00
jschoubben a036bac47b Record why the module's own secret is named for whose it is
The field was called needs, beside secrets, and both were name-to-path holding
something secret. What separates them is whose, not how secret — so that is
what the name says now.
2026-08-31 13:47:21 +02:00
jschoubben aafeb5c9df Three issues resolved: one closed by evidence, two answered by the replacement
012 named its own closing condition — a scenario with four images coming up —
and the scenario now stocks seven and has raised cleanly many times at the
memory the wrong diagnosis had raised.

001 is answered by the host reading the package database back after installing.
002 was NOT answered and was present here too, so it is a fix rather than a
note: a stale index is now named instead of reported as a failed install.
2026-08-31 13:00:03 +02:00
jschoubben 573a94e102 Correct the record: a limitation that no longer exists, and one that was never written
The connectivity design still said a hub cannot be filtered — a gap recorded in
the morning and closed in the afternoon, left standing as though it were
current. Worse than a stale date: it would send somebody away from something
that works.

`restart-on` was described nowhere, including the part added today that lets a
service reflect a file another module put on the machine. A rule the host
enforces and no document mentions is a rule nobody can rely on.

And nine of fifteen design documents claimed an `updated:` older than their last
change, some by a week. That field is what cross-cutting views are generated
from, so it is not decoration.
2026-08-31 12:33:35 +02:00
jschoubben f4e81074d3 Which resolver is a claim, and was decided before it was asked
A resolver takes over /etc/resolv.conf, which is a singular resource — ADR 0009
lists it in the table beside the seat and pid 1. So choosing between resolved,
dnsmasq and unbound is assigning a module, per machine, and the mesh refuses
two rather than letting them fight over the file.

Recorded because it was treated as an open question two days after being
decided, which is the argument for that table being a table.
2026-08-31 12:15:12 +02:00
jschoubben a3cee17d48 Record what a container can see of the mesh's names, and what it cannot
Found by a container failing to resolve a name every machine could: a container
gets its own hosts file holding only its own hostname, and on the machine it
always worked, which is what made it easy to miss.

Declared containers are given the names. A container somebody starts by hand is
not the mesh's to configure — which is a second, different reason to want a
resolver, recorded beside the first rather than folded into it.
2026-08-31 11:29:25 +02:00
jschoubben 35ce23fb76 Record that a machine coming back is ordinary, and what waking now does
Asked whether a machine that drops off needs re-adopting: it does not, nothing
expires, and the only thing that forces re-enrolment is losing its own key.

The gap was the twenty or thirty seconds after a resume in which a node
believes it is in a mesh it has left — recovering on its own, which made it a
quality gap rather than a fault, and still a machine waiting to be told
something it already knew.
2026-08-31 10:21:50 +02:00
jschoubben 6374c1eb60 Record what "behind" means now
It meant failed-or-refused, so the question this record says must not be lost
was answerable only for the machines that broke. Out of date, never told, and
not worked out are kept apart: the remedy is the same push and they read
differently to whoever is looking.
2026-08-31 05:28:11 +02:00
jschoubben 7c0be6968e Record what a machine says about itself and what the mesh keeps
The yes gates an assignment and the detail carries a value; they are one fact
read two ways, and only one read was being kept.
2026-08-31 04:54:07 +02:00
jschoubben 7f314eb399 Point three design documents at the code that exists for them
Their subject matter has been built and proven for days and their frontmatter
still said code: [] — which is what the cross-cutting view is generated from,
so it was claiming nothing existed for the substrate, the node lifecycle and
delivery.
2026-08-31 04:51:04 +02:00
jschoubben 5ad3641cbf Record the board, which is the last designed document with no code
One reading answered three ways, holding nothing and touching no context's
store — which is the constraint the whole document is about, and the thing the
board being replaced gets wrong.
2026-08-31 04:50:45 +02:00
jschoubben 0bc4b7774f Record what four more pieces of the mesh became
Rotation and the provisioner contract; model access as a provision answered by
a record, with ADR 0024's other two gaps left as gaps; exposure, which closes
the open question about revoking a route; and the delivery loop, which closes
the gap ADR 0010 left when it replaced a pipeline with a comparison.
2026-08-31 02:56:50 +02:00
jschoubben 6b1c80b442 What a builder-as-a-module can and cannot reach
The broker's fingerprint travels with its credential, and the machine's
filesystem does not travel at all — it runs in a container, which is the
arrangement working rather than a limitation to route around.
2026-08-31 01:53:35 +02:00
jschoubben 8448219de1 Issue 018: a provider on the same machine was never announced to its consumer 2026-08-31 01:35:54 +02:00
jschoubben 00e98f1f92 The vocabulary has no word for a unit that runs and exits
Found by the firewall: every packet filtered as declared, and the machine
reported as not doing what it was told, because the unit that loaded the rules
had finished. Stated as a gap rather than worked around silently.
2026-08-31 01:20:58 +02:00
jschoubben 0ac99f0d68 An action's own idea of being finished must be its verify's
Otherwise it succeeds into a state its verify rejects, and the host's report is
accurate and names nothing. Recorded where the vocabulary is described, because
it is a rule about writing an action rather than about one action.
2026-08-31 01:03:50 +02:00
jschoubben 1ade18209d Issue 017: an action succeeded into a state its own verify rejects 2026-08-31 01:01:17 +02:00
jschoubben a9cd3de5be Issue 016: anything after the declaration in a file was ignored 2026-08-31 00:51:46 +02:00
jschoubben 6e7e77acbe Issue 015: the harness read a swallowed answer as success 2026-08-31 00:48:16 +02:00
jschoubben 890c3ee3fc State the one rule the derivation does not yet reach
A hub needs its overlay port open and a node that is not a hub does not, and
they are the same module — so listens, a static manifest field, cannot express
it while the overlay module's resources are computed per node. Written down
rather than left as an oversight for whoever first puts a firewall on a hub.
2026-08-31 00:42:39 +02:00
jschoubben a6872ac099 A key that is present and unusable, and what the certificate work became
Issue 014: the node's serving key was stored in the host's own encoding, so
every check that reads the file passed and no server could start. Same shape as
013 — two halves of one mechanism designed separately, each correct about its
own half. Where a file exists so a third party can read it, the format is the
interface.
2026-08-31 00:42:13 +02:00
jschoubben 778efaba8b The mesh runs its own registry, certifies its own names, and computes its own filtering
Issue 003 is answered in both halves: manifests are parsed strictly, and a
module says what it listens on and from where rather than carrying a key
nothing reads. The design records what was built and how each part is checked.

Issue 013 is new, found by reading while writing the first module that has
both a computed file and a service that needs it. The file arrived second.
It failed, then the next reconcile fixed it, which is why nothing caught it.
2026-08-31 00:37:34 +02:00
642 changed files with 49382 additions and 8938 deletions
+7 -4
View File
@@ -23,10 +23,13 @@ and says they disagree.
## Amending the to-be layer ## Amending the to-be layer
1. Write the decision record. If it reverses an earlier one, that record gets 1. Propose the decision record in the knowledge base (`hq-graduate`, steps 2 and 3). If it reverses an
`status: superseded` and `superseded-by:` — its text is never edited. earlier one, it `supersedes` it, or `changes` the clauses it replaces — the earlier record's text is
2. Edit the design; set `updated:` to today. never edited.
3. If this came from an issue, set that issue's `amended-design:`. 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 ## Amending the as-is layer
+78
View File
@@ -0,0 +1,78 @@
---
name: hq-defer
description: Use when a thought is raised that should be remembered but NOT worked on now — an aside during other work, a "we should look at X someday", a known gap nobody is assigning yet. Triggers on "defer this", "park this", "register this thought", "note this for later", "don't work on it, just remember it". Records and returns to whatever was already in progress.
---
# hq-defer
Parks a thought so it is not lost, **without moving the work off course.** The defer is the
point: the thought is recorded and the previous task resumes.
This skill wraps no playbook, because deferring is not part of the development cycle — it is
what happens *before* something enters it. A parked thought has no number, no owner and no
status, and that is correct.
## Where it goes, and why not the repository
Record it as **one memory file** in Claude's persistent memory directory for this project (the
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`, 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.
- A shared "deferred" or "someday" document is a **central status file**, which
[`AGENTS.md`](../../../AGENTS.md) forbids. Status lives in frontmatter on real records, and a
parked thought has no real record yet.
- A repository write means a branch, a commit and a pull request — drift, which is the one thing
this skill exists to avoid.
## Steps
1. Write the memory file. Slug is kebab-case and descriptive of the thought, not of the act of
deferring.
```markdown
---
name: <kebab-slug>
description: Deferred note — <one line>
metadata:
type: project
---
Raised and deliberately deferred on YYYY-MM-DD: **<the thought, in the user's own terms>**
**Why:** what was being worked on when it came up, and that deferring was intentional so
that work was not pulled off course.
**How to apply:** treat as an open thread, not an assignment. Do not start on it
unprompted. If it graduates it needs an HQ home first — an issue under playbook
[03](../../../00-META/process/03-issues.md) if a stated behaviour does not happen, or
research under playbook [01](../../../00-META/process/01-research.md) if it is still an
idea. Say which is undetermined, if it is.
```
2. Append one line to `MEMORY.md`: `- [<Title>](<kebab-slug>.md) — deferred YYYY-MM-DD; parked, no HQ
record, do not start unprompted`.
3. Convert relative dates to absolute before writing. "Last week" is worthless in six months.
4. Check for an existing memory covering the same thought and update it instead of adding a
duplicate.
## Then stop
Reply in **at most two lines** — what was recorded, and that it is parked — and **return to
whatever was in progress before.** Do not summarise the parked thought back at length, do not
propose a plan for it, do not ask which playbook it belongs to, and do not open anything.
If nothing was in progress, say only that it is recorded.
## Do not
- Do not create an issue, a research effort, a decision record or a design document.
- Do not create a branch, commit or pull request.
- Do not start investigating the thought, however cheap the first check looks.
- Do not name nodes, domains, addresses, absolute paths or usernames in the memory file — the
thought may later be quoted into this repository, which is public.
- Do not decide whether it is an issue or research when the evidence does not say. Recording
"undetermined" is the honest outcome and costs nothing later.
+13 -8
View File
@@ -23,14 +23,19 @@ result is not "nothing to learn" — it is the reason the next person repeats th
## Steps ## Steps
1. Read `00-report.md`. Move `status:` to `diagnosing`. 1. Read the issue: `mesh_call issue-tracker.get` with its number, and `00-report.md` when it has a
2. Investigate. Write `01-diagnosis.md` in the same folder: the trail, **dated**, including folder. Before you start, move it to `diagnosing` with `issue-tracker.move` and `by`, your name and node as
what was ruled out and how. Archaeology — which commit, which pull request, which date a who@node (such as `fix-399@laptop`). The tracker refuses it while another worker is current, naming them.
behaviour changed — is the most valuable content here. 2. Investigate. The trail goes in `issue-tracker.note`, **dated** in its text, including what was
3. When the owner is known, set `status: located` and fill `located-in:` with repositories or ruled out and how; for an issue with a folder, in `01-diagnosis.md` there as well. Archaeology —
modules from [`00-META/repos.md`](../../../00-META/repos.md). which commit, which pull request, which date a behaviour changed — is the most valuable content.
4. On resolution: `status: resolved`, fill `fixed-by:`. If the root cause was a design gap, run 3. When the owner is known, `issue-tracker.move` to `located` with `located-in`: entries of the
`hq-graduate` for the amendment and fill `amended-design:`. 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 ## 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. Closes a research effort into a decision and a design, or amends an existing design.
**Authoritative playbook:** **Authoritative playbook:**
[`00-META/process/02-graduation.md`](../../../00-META/process/02-graduation.md) — read [`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 ## Steps
1. **Check against GENESIS** — `mission.md`, `context.md`, `effect.md`. If the conclusion 1. **Check against GENESIS** — `mission.md`, `context.md`, `effect.md`. If the conclusion
conflicts, say which is wrong, in writing, before proceeding. conflicts, say which is wrong, in writing, before proceeding.
2. **Write the decision record** in `02-DECISIONS/`, next free number, frontmatter per 2. **Reserve the record's number.** `mesh_call knowledge-base.reserve {"kind": "decision", "for": "<title>"}`.
[`02-DECISIONS/README.md`](../../../02-DECISIONS/README.md). `reconstructed: false` — this one is being taken, 3. **Propose the decision record.** `mesh_call knowledge-base.propose` with the reserved `number`, `title`,
not recovered. Record the rejected options. `topic` (one of the six in [`02-DECISIONS/README.md`](../../../02-DECISIONS/README.md)), `date`,
3. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`, `deciders`, `clauses` (each `"**Lead.** the statement"`), `sections` (Context, Considered Options,
empty `code:`, today's `updated:`, and `decisions:` pointing at the record. Consequences, How it is checked, References), `links` (`extends`, `rests-on`, `supersedes`, `changes` with
4. **Close the effort** — the research `00-overview.md` gets `status: graduated` and `became:` `"NNNN §N"`, `cites`), `homes` (the design's path), and `needs-operator: true` when the operator must
pointing at both. 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 ## Amending an existing design
A design changes only through a decision. Write the record first; if it reverses an earlier A design changes only through a decision. Propose the record first; if it reverses an earlier one,
one, set that record's `status: superseded` and `superseded-by:` — **never edit its text**. it `supersedes` it (or `changes` the clauses it replaces) — **never edit the earlier record's text**;
Then edit the design and set `updated:`. 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 ## 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 move a to-be document into `00-as-is/`.
- Do not edit an as-is document to describe an intention. - Do not edit an as-is document to describe an intention.
- Do not change a decision record's meaning. Supersede it. - 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 ## Steps
1. **Confirm it is settled.** `status: designed`, and every claim traceable to a record in 1. **Confirm it is settled.** `status: designed`, and every claim traceable to a decision record in the knowledge base
`02-DECISIONS/`. An open question in the text is a reason to run `hq-new-research`, not to build (`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. around it.
2. **Name the owner.** Set `code:` from 2. **Name the owner.** Set `code:` from
[`00-META/repos.md`](../../../00-META/repos.md). If the repository does not exist yet, [`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 there, this is not an issue — it is a lookup. If the answer is a general lesson, it belongs in
both. 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 ## 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 ```yaml
--- ---
status: open
opened: YYYY-MM-DD 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 — then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix
what was run, what came back, when. live in the tracker (ADR 0275), and `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`,
3. Say why it matters beyond the instance. An issue that is only one occurrence is a knowledge `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. 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
- Do not name nodes, domains, addresses or paths. This repository is public. - 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 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 # 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; [`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 ## Steps
1. Find the next free number: list `01-RESEARCH/NNN-*`, take the highest plus one, zero-padded All through the mesh MCP server (`mesh_describe` gives each verb's arguments).
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**:
```yaml 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,
status: active `reasoning: true` for its documents).
initiated: YYYY-MM-DD 2. **Reserve the number.** `mesh_call knowledge-base.reserve {"kind": "research", "for": "<title>"}`. The
touches: [] number is spent; never pick one by hand.
became: [] 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 in the same folder. Keep the summary current. 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
- Do not restate the status in prose. It lives in frontmatter, in one place. - Do not restate the status in prose. It is the effort's `status`, in one place.
- Do not fill `became:` while the effort is open — `hq-graduate` sets it at closure. - Do not give a `became` link while the effort is open — `hq-graduate` gives it at closure.
- Do not skip or reuse a sequence number.
- Do not write into `03-DESIGN` from an open effort. - 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. - Do not name the mesh being observed. Evidence is required; identification is forbidden.
## Closing ## Closing
An effort never just stops. It closes through `hq-graduate` as `graduated` or `abandoned`, 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 # 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 workflow — HQ has **no central status file by design** (decision 35). Every view is
generated on demand and never written back to disk. 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 | | 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` | | 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` | | 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 | `04-ISSUES/NNN-*/00-report.md` | `status` (`open` / `diagnosing` / `located` / `resolved` / `wontfix`), `opened`, `located-in`, `fixed-by`, `amended-design` | | 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 ## Steps
1. Read the frontmatter block from every file above — a grep across each tree is enough, no 1. Read the designs' frontmatter (a grep across `03-DESIGN/` is enough, no need to load bodies), the
need to load bodies. issues from the tracker, and the records from the knowledge base. An issue report carries no status;
2. Render what was asked as Markdown tables. Group and sort sensibly. The **ADR index** is one where an issue stands is only in the tracker (ADR 0275). A decision record's or research effort's
of these views: records ordered by number, with title, date and status, and reconstructed status is only in the knowledge base (ADR 0290).
ones marked. 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: 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:` - 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 - 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 - an as-is design whose claims are older than its `updated:` date suggests
## Do not ## Do not
- **Do not write a status file.** Central status files are explicitly rejected. The view is - **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 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. already drifted after a single addition, which is why it was removed, and the generated one
- Do not infer status from prose. Trust only the frontmatter; if it is wrong, flag it. 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. 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 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. 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 ## Do not
+1
View File
@@ -0,0 +1 @@
__pycache__/
+4 -3
View File
@@ -9,6 +9,7 @@ works — plus the engineering practice that holds across everything Novox build
| [`context.md`](context.md) | The environment — conditions, not aspirations | | [`context.md`](context.md) | The environment — conditions, not aspirations |
| [`effect.md`](effect.md) | What is different when the work is done | | [`effect.md`](effect.md) | What is different when the work is done |
| [`how-we-build.md`](how-we-build.md) | The rules that hold across the mesh, each one earned. **The source of the mesh constitution** — the governed page the mesh injects into design sessions is derived from it. | | [`how-we-build.md`](how-we-build.md) | The rules that hold across the mesh, each one earned. **The source of the mesh constitution** — the governed page the mesh injects into design sessions is derived from it. |
| [`glossary.md`](glossary.md) | One name per thing — the authority on vocabulary, and the words that were retired |
| [`repos.md`](repos.md) | Where implementation lives, and what each repository owns | | [`repos.md`](repos.md) | Where implementation lives, and what each repository owns |
| [`process/`](process/) | The playbooks — how work moves through this repository, for engineers and agents alike | | [`process/`](process/) | The playbooks — how work moves through this repository, for engineers and agents alike |
@@ -34,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 - 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 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 - It listed the mesh as spanning a fixed number of named machines, which is exactly the
content this repository cannot carry. content this repository cannot carry.
@@ -44,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. 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 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). [`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 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 A founding document contradicting the direction of travel is precisely the failure this folder
exists to prevent. exists to prevent.
+89 -22
View File
@@ -1,9 +1,12 @@
# Checks # Checks
``` ```
python3 00-META/checks/records.py structure: links, citations, supersession, topics 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 in 02-DECISIONS/README.md is current python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
python3 00-META/checks/index.py --write regenerate it 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. Non-zero exit on any problem, so it can be a gate rather than a report.
@@ -20,30 +23,94 @@ indistinguishable from one that cannot.
| Check | Asserts | Found | | Check | Asserts | Found |
|---|---|---| |---|---|---|
| `links` | every relative link resolves | — (run ad hoc during authoring; now permanent) | | `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 | | `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 |
| `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 | | `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 |
| `supersession` | if A says it was superseded by B, B says it supersedes A | ADR 0012 never declared that it superseded 0011 | | `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 |
| `numbering` | the number in the filename is the number in the heading | — | | *(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 |
| `topics` | every record names a topic the index knows | — | | `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 |
| *(index.py)* | the written reading order matches what the records say | — |
## 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 ## What is deliberately not checked
- **`02-DECISIONS/` and `01-RESEARCH/` may cite superseded records freely.** A decision record - **Whether a record a document cites exists, or is still accepted.** A checkout cannot read the knowledge
discusses history; research records what was observed. Flagging those would produce noise on base, and a check that called the mesh would make every merge here depend on it. Until designs become the
correct documents, and a check that cries wolf gets suppressed — which costs more than not knowledge base's own kind (issue 428), the reviewer asks `knowledge-base.get` for each record a change
having it. cites in `00-META/` or `03-DESIGN/01-to-be/` (the documents that tell somebody what to do). An as-is
- **`03-DESIGN/00-as-is/` may rest on a superseded record.** It describes what runs, and what document may rest on a superseded record: it describes what runs, and what runs was built under whatever
runs was built under whatever was decided at the time was decided at the time (ADR 0006: *as-is describing a superseded decision is exactly what as-is is for*).
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md): - **Whether a citation's prose is still true.** A document can cite an accepted record and describe it
*as-is describing a superseded decision is exactly what as-is is for*). wrongly, and nothing here notices.
- **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.
## Adding a check ## Adding a check
State what incident it would have caught, and make it fail before you make it pass. A check State what incident it would have caught, and make it fail before you make it pass. A check
whose failure has never been observed is a guess about its own correctness. whose failure has never been observed is a guess about its own correctness.
## 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.
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""The development cycle, checked.
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 a status this repository holds lives in frontmatter and nowhere else.
What is enforced:
design every 03-DESIGN doc parses, carries `layer:` matching its directory, and a
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 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.
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
"""
import glob
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "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):
return os.path.relpath(path, ROOT)
def frontmatter(path):
"""The YAML block between the first two --- lines, as {key: raw-value-string}.
Minimal on purpose, like records.py: enough for the fields these checks read. A list
value (block or inline) is joined into its items; a scalar stays a string.
"""
text = open(path, encoding="utf-8").read()
m = re.match(r"^---\n(.*?)\n---", text, re.S)
if not m:
return None
front, out, key = m.group(1), {}, None
for line in front.split("\n"):
item = re.match(r"^\s+-\s*(.+?)\s*$", line)
if item and key:
out[key].append(item.group(1))
continue
kv = re.match(r"^([A-Za-z-]+):\s*(.*)$", line)
if not kv:
continue
key, value = kv.group(1), kv.group(2).strip()
if value.startswith("[") and value.endswith("]"):
out[key] = [v.strip() for v in value[1:-1].split(",") if v.strip()]
elif value == "":
out[key] = [] # a block list may follow; stays [] if nothing does
else:
out[key] = value
return out
def listy(front, key):
v = front.get(key)
if v is None:
return []
return v if isinstance(v, list) else ([v] if str(v).strip() else [])
def main():
failures = []
def bad(path, why):
failures.append(" %s: %s" % (rel(path), why))
# ---- design ------------------------------------------------------------------------
for layer, name in (("00-as-is", "as-is"), ("01-to-be", "to-be")):
for path in sorted(glob.glob(os.path.join(ROOT, "03-DESIGN", layer, "*.md"))):
if os.path.basename(path) == "README.md":
continue
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
if front.get("layer") != name:
bad(path, "layer is %r; this directory is %s" % (front.get("layer"), name))
status = front.get("status")
if status not in DESIGN_STATUSES:
bad(path, "status %r is not one of %s" % (status, sorted(DESIGN_STATUSES)))
if name == "to-be":
if not listy(front, "decisions"):
bad(path, "names no decisions -- no design without a decision")
if status in ("in-progress", "implemented") and not listy(front, "code"):
bad(path, "status %s but code: names no owner -- no development "
"without a design that says where" % status)
# ---- issues ------------------------------------------------------------------------
# Two records may not share a number. Numbers are taken as "next free after main", and work
# sits on unmerged branches for days -- so two people reading the same main allocate the same
# number, and nothing said so. It happened twice in one evening between two machines, and the
# second collision landed on main with all three checks passing (issue 155). An issue number is
# how every other record cites this one; two records answering to it means a pointer that
# resolves to whichever the reader happened to open.
seen = {}
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
name = os.path.basename(os.path.normpath(folder))
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen:
bad(os.path.join("04-ISSUES", name),
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
"records answering to one means a citation that resolves to whichever the reader "
"opened. Take the next free number across main AND every open pull request"
% (number, seen[number]))
else:
seen[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
opened = str(front.get("opened") or "")
if not opened:
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
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")))
)
if failures:
print("cycle: %d document(s) break the development cycle:" % len(failures))
print("\n".join(failures))
return 1
print("cycle: %d documents checked, the chain holds" % checked)
return 0
if __name__ == "__main__":
sys.exit(main())
+48 -82
View File
@@ -1,106 +1,72 @@
#!/usr/bin/env python3 #!/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 A number identifies a record and never changes, so the reading order is not the order of the numbers.
than reading order. The index is what carries the path — and it is written rather than only It is the six topics `02-DECISIONS/README.md` writes down, in order, and each record's own topic. The
generated on demand, because a reader on a forge sees the folder and not a command. 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 python3 00-META/checks/index.py check
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 --write regenerate it It fails when the README's reading order is missing or names a topic twice, and when the README stores
python3 00-META/checks/index.py fail if it is stale 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 io
import os
import re import re
import sys import sys
README = "02-DECISIONS/README.md" 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. # A topic in the README's reading order, e.g.
TOPICS = [ # 1. **What the mesh is** — `topic: the mesh`. What it is for, ...
("the mesh", "What the mesh is"), # The knowledge base (mesh-knowledge, reference.go) reads the same line, so the form is a contract.
("the tiers", "Its tiers, from the bottom up"), TOPIC_LINE = re.compile(r"^\d+\.\s+\*\*(?P<label>[^*]+)\*\*\s+—\s+`topic:\s*(?P<topic>[^`]+)`", re.M)
("what runs on it", "What runs on them, and how it gets there"),
("building it", "How it is built"), # What a stored list looks like: the old markers, or a line of the list index.py used to print.
("checking it", "How it is checked"), STORED = [
("how we work", "How we work"), (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): def reading_order(text=None):
m = re.search(r'^%s:\s*(.+)$' % name, text, re.M) """The topics, in order, as (topic, label) pairs, from the README."""
return m.group(1).strip() if m else None 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 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 main(): 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() 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: problems = []
print("index: %s has no index markers (%s / %s)" % (README, START, END)) 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 return 1
print("index: %d topics in the reading order, no stored list" % len(topics))
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")
return 0 return 0
+96 -170
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 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: 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 python3 00-META/checks/records.py
Exits non-zero if anything fails, so it can be a gate rather than a report. 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 import os
@@ -15,14 +20,17 @@ import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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:)([^)]+)\)") 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(): def markdown_files():
@@ -45,7 +53,7 @@ def read(path):
def frontmatter(text): def frontmatter(text):
"""Minimal frontmatter reader — enough for the fields these checks use. """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. scalar fields and one list, would cost more than it returns.
""" """
if not text.startswith("---\n"): if not text.startswith("---\n"):
@@ -74,24 +82,11 @@ def frontmatter(text):
return fields return fields
def load_records(): def unquote(value):
"""Every decision record, by its four-digit number.""" value = value.strip()
records = {} if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
folder = os.path.join(ROOT, "02-DECISIONS") return value[1:-1]
for name in sorted(os.listdir(folder)): return value
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
class Failures: class Failures:
@@ -129,171 +124,102 @@ def check_links(failures):
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}") failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
def check_rests_on(failures, records): def check_no_records(failures):
"""A document's `decisions:` and a record's `extends:` must name a live record. """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 The knowledge base is the only place a record is read; a file here would be a second copy that
decision. Resting on a withdrawn one is the defect this whole check set exists for. drifts from it, and nothing would read it. The folders keep their pointer READMEs and nothing else.
""" """
for path in markdown_files(): for folder in RECORD_FOLDERS:
front = frontmatter(read(path)) base = os.path.join(ROOT, folder)
cited = list(front.get("decisions", []) or []) if not os.path.isdir(base):
extends = front.get("extends") failures.add("no-records", folder, "the folder is gone; it keeps a README that points at the "
if isinstance(extends, str) and extends: "knowledge base (and, for 02-DECISIONS, the reading order the knowledge base reads)")
cited.append(extends) continue
for current, dirs, files in os.walk(base):
for entry in cited: for name in sorted(files + [d + "/" for d in dirs]):
match = ADR_FILE.match(os.path.basename(entry)) path = rel(os.path.join(current, name))
if not match: if path.rstrip("/") in POINTERS:
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
# 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):
continue continue
failures.add( failures.add(
"rests-on", "no-records",
rel(path), path,
f"rests on ADR {number}, which is '{status}' — a document may not rest on a " "a decision record or research effort lives in the knowledge base, never here: "
f"record that is not accepted", "write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)",
) )
def check_live_citations(failures, records): def check_decisions_entries(failures):
"""In a governing document, a link to a superseded record must name its replacement. """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. A to-be design rests on the records it names, and the knowledge base answers for a record by its
Naming the superseder in the same paragraph is both, and it is what a person would number; an entry in another form is a citation nothing can follow.
write anyway.
""" """
superseded = {
number: record["front"].get("superseded-by", "")
for number, record in records.items()
if record["front"].get("status") == "superseded"
}
for path in markdown_files(): for path in markdown_files():
if not any(rel(path).startswith(prefix) for prefix in GOVERNING): if not rel(path).startswith("03-DESIGN/"):
continue 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) text = read(path)
offset = 0 for number, line in enumerate(text.split("\n"), 1):
for paragraph in text.split("\n\n"): line = re.sub(r"`[^`]*`", "", line)
line_no = text[:offset].count("\n") + 1 for m in ADR_MENTION.finditer(line):
offset += len(paragraph) + 2 if len(m.group(1)) != 4:
targets = LINK.findall(paragraph) failures.add("mention-form", f"{rel(path)}:{number}",
for target in targets: f"{m.group(0)!r}: a decision record is named 'ADR NNNN', four digits")
match = ADR_FILE.match(os.path.basename(target.split("#")[0])) for m in RESEARCH_MENTION.finditer(line):
if not match or match.group(1) not in superseded: if len(m.group(1)) != 3:
continue failures.add("mention-form", f"{rel(path)}:{number}",
number = match.group(1) f"{m.group(0)!r}: a research effort is named 'research NNN', three digits")
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): def check_status_against_code(failures):
"""If A says it was superseded by B, B must say it supersedes A.""" """A design document naming specific code may not still call itself `designed`.
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: **Naming a file is a claim that the file implements this**, so the two fields have to agree.
failures.add("supersession", rel(record["path"]), "marked superseded but names no superseder") They drifted: ten to-be documents named working, lab-proven code — several with a *What was
continue built* or *Raised, and observed* section — while still saying nothing had been built.
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) Deliberately weak, and that is the point of it being mechanical. It cannot tell whether the
if not match or match.group(1) not in records: prose is true, only that a document has stopped claiming to be unbuilt once it points at
failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}") something. `code: [mesh-controller]` — a repository with no path — is a plan and stays
continue `designed`.
other = records[match.group(1)]
claims = os.path.basename(str(other["front"].get("supersedes", "")))
if claims != record["name"]:
failures.add(
"supersession",
rel(other["path"]),
f"ADR {number} says this supersedes it; this record does not say so "
f"(supersedes: {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", for path in markdown_files():
"how we work"} if not rel(path).startswith("03-DESIGN/01-to-be/") or path.endswith("README.md"):
for number, record in sorted(records.items()): continue
topic = record["front"].get("topic") front = frontmatter(read(path))
if not topic: if front.get("status") != "designed":
failures.add("topics", rel(record["path"]), continue
"no topic, so it has no place in the reading order") for entry in front.get("code") or []:
elif topic not in known: named = re.sub(r"\s*\(.*\)$", "", entry).strip().split(None, 1)
failures.add("topics", rel(record["path"]), if len(named) > 1:
"topic %r is not one of: %s" % (topic, ", ".join(sorted(known)))) failures.add(
"status-vs-code",
rel(path),
def check_numbering(failures, records): f"`designed`, but names {named[1]!r} in {named[0]}. Naming a file claims "
"""The number in the filename is the number in the heading.""" f"it implements this — use `in-progress`, or `implemented` once it is "
for number, record in records.items(): f"defensible from that repository's main branch.",
heading = re.search(r"^# (\d+)\.", record["text"], re.M) )
if not heading: break
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 main(): def main():
failures = Failures() failures = Failures()
records = load_records()
check_links(failures) check_links(failures)
check_rests_on(failures, records) check_no_records(failures)
check_live_citations(failures, records) check_decisions_entries(failures)
check_supersession_symmetry(failures, records) check_mentions(failures)
check_numbering(failures, records) check_status_against_code(failures)
check_topics(failures, records)
print(f"records: {len(records)} decision records checked")
return failures.report() 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
@@ -30,7 +30,7 @@ named, and nothing should be designed around a particular one existing.
- **A hosted model provider** supplies the thinking for non-human agents, drawn from a - **A hosted model provider** supplies the thinking for non-human agents, drawn from a
shared pool of subscriptions — which is why budget pacing is a first-class concern. shared pool of subscriptions — which is why budget pacing is a first-class concern.
- **Long-lived user services** rather than an orchestrator. No cluster scheduler, no cloud - **Long-lived user services** rather than an orchestrator. No cluster scheduler, no cloud
control plane. controller.
Defaults, not mandates. A second model provider is anticipated by design; nothing in the Defaults, not mandates. A second model provider is anticipated by design; nothing in the
domain may assume one vendor's credential lifecycle. domain may assume one vendor's credential lifecycle.
+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 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 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. written months ago.
The interface is intent. The mesh handles the rest. The interface is intent. The mesh handles the rest.
+726
View File
@@ -0,0 +1,726 @@
# 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 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).
## How to read this page
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.
Two fixed lines follow an entry where they apply, and `00-META/checks/words.py` reads them:
- *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).
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).
- **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.
## Placement — what runs on which node
**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).
- **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).
- **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.
- **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).
## Identity and access — who may do what
**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 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 status: canonical
updated: 2026-08-23 updated: 2026-10-11
derives: knowledge-base constitution page derives: the constitution page
decisions: decisions:
- 02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md - "ADR 0020"
--- ---
# How we build # 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 | | 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. | | **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 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. | | **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. | | **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 ### 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 - **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. declare is invisible to the mesh: it will not be generated, injected, or audited.
- **Provisioned credentials arrive through declared requirements**, never hardcoded in code, - **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 - **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 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 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, - **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 registered as a build source. Creating an application directory in the monorepo is a
convention violation and reviewers reject it. convention violation and reviewers reject it.
[ADR 0015](../02-DECISIONS/0015-applications-live-in-their-own-repository.md) ADR 0015
### Migrations ### 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 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 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 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. 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. 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 ### 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 ### Search the record before forming a hypothesis
The first action on any error message, failing service or unexpected behaviour is to search the 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 record for the literal error text (`records.records_search`) — before a hypothesis, not after one fails.
knowledge base is indexed on symptoms. 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 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 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 1. **Propose** — a change stating what rule is changing, why the current wording is
inadequate, and what reviewed it. inadequate, and what reviewed it.
2. **Review** — sign-off by reviewers who are not the proposer. 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. rule the mesh enforces is architecturally significant.
4. **Sync** — playbook [`process/05-constitution-sync.md`](process/05-constitution-sync.md) 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 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 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 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 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 ## 8. Code quality
*Absorbed 2026-08-26 from the enforced page, which carried these rules while this document did *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.** **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, 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 *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 — 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. - TypeScript throughout; no new untyped JavaScript.
- Strict, with no implicit `any` and no unchecked index access. - 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 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 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. no remembering which node holds which thing.
Not automation, which does what it was told to do in advance. Self-control: the mesh Not automation, which does what it was told to do in advance. Self-control: the mesh
+38 -12
View File
@@ -1,6 +1,10 @@
# Process — overview # Process — overview
How work moves through HQ, and who may do what. Every other document in this folder is a 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,
ADR 0081). Every other document in this folder is a
playbook: trigger, who runs it, steps, outputs. Engineers and agents follow the same playbook: trigger, who runs it, steps, outputs. Engineers and agents follow the same
playbooks; agents must not act outside them. playbooks; agents must not act outside them.
@@ -15,11 +19,13 @@ playbooks; agents must not act outside them.
## The knowledge flow ## The knowledge flow
``` ```
idea ──► 01-RESEARCH ──► decision (02-DECISIONS/) ──► 03-DESIGN/01-to-be ──► built (code repo) idea ──► research ──► decision ──► 03-DESIGN/01-to-be ──► built (code repo)
│ │ │ │ │ │
│ │ └─► 03-DESIGN/00-as-is once shipped │ │ └─► 03-DESIGN/00-as-is once shipped
│ └────► abandoned (recorded, kept) │ └────► 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 symptom ──► 04-ISSUES ──► diagnosis ──► code-repo fix and/or design amendment
@@ -33,7 +39,7 @@ how-we-build.md ──► constitution sync ──► knowledge base ──► i
| Layer | What it is | Changes when | | 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. | | `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 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, to-be document's frontmatter goes to `implemented`, and both stand — one describing what runs,
@@ -49,12 +55,32 @@ the expensive half.
| [03](03-issues.md) | Issues | Something is wrong — often with the owner unknown | | [03](03-issues.md) | Issues | Something is wrong — often with the owner unknown |
| [04](04-build-handoff.md) | Build handoff | A design is ready to be built | | [04](04-build-handoff.md) | Build handoff | A design is ready to be built |
| [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces | | [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces |
| [06](06-writing-a-module.md) | Writing a module | Something that runs today must run on the mesh |
| [07](07-feature-branches.md) | Feature branches across repos | Work that changes code, in one repo or several at once |
## Status lives in frontmatter ## The cycle is checked
Research overviews, design docs, issue reports and decision records each carry their status as The flow above is a rule, and a rule states how it is checked:
YAML frontmatter (schemas in the section READMEs and playbooks). There are **no central status [`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, 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, an issue's in the tracker
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 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 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, the decision index included, it is not worth a record it is not recorded. Cross-cutting views are generated on demand by the
are generated on demand by the `hq-status` skill and never written to disk. `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. **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 ## Steps
1. Take the next free number: highest `01-RESEARCH/NNN-*` plus one, zero-padded to three 1. **Read what is known first.** `knowledge-base.ask` with the question in plain words returns the decision
digits. Never skip or reuse a number. records and research efforts that bear on it; `knowledge-base.get` with `kind: research` and a number
2. Create `01-RESEARCH/NNN-descriptive-name/` and a `00-overview.md` in it carrying: 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 `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.
status: active The knowledge base then reviews the proposal itself, through a judgement that sees only the record, the
initiated: YYYY-MM-DD records it links and the rules; on `changes` the effort stays proposed with the findings, and its author
touches: [] # areas, subsystems or as-is design docs the effort bears on 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.
```
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.
## What makes research worth reading ## What makes research worth reading
@@ -37,12 +46,21 @@ carries the whole lesson without naming anything.
## Do not ## 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 put status in prose. It is the effort's `status`, and the prose must not restate it.
- Do not set `became:` while the effort is open — playbook 02 sets it at closure. - 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 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 ## Closing
An effort never just stops. It closes through playbook [02](02-graduation.md) as `graduated` 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. 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.
+91 -12
View File
@@ -4,15 +4,20 @@
**Who runs it.** Anyone, with the decision recorded before the design moves. **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 ## Graduating research
1. **Check it against GENESIS.** An effort graduates only if its conclusion is traceable to 1. **Check it against GENESIS.** An effort graduates only if its conclusion is traceable to
[`mission.md`](../mission.md), [`context.md`](../context.md) and [`mission.md`](../mission.md), [`context.md`](../context.md) and
[`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is — [`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is —
say which, in writing, before proceeding. say which, in writing, before proceeding.
2. **Record the decision.** Write a record in [`02-DECISIONS/`](../../02-DECISIONS/) taking the next free 2. **Record the decision** (below, *Writing a decision record*).
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.
3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter: 3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter:
```yaml ```yaml
@@ -21,22 +26,83 @@
status: designed status: designed
code: [] # owning code repo(s); set at build handoff, empty before code: [] # owning code repo(s); set at build handoff, empty before
updated: YYYY-MM-DD 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 4. **Close the effort.** `knowledge-base.propose` the effort again, whole, with `status: graduated` and a
`became:` pointing at the design document and the decision record. `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 ## Amending an existing design
A design changes only through a decision. A design changes only through a decision.
1. Write the decision record. If it reverses an earlier one, the earlier record's `status:` 1. Write the decision record (above). If it reverses an earlier one, it `supersedes` it.
becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its text is never edited**. 2. Edit the to-be design document, add the record to its `decisions:` as `"ADR NNNN"`, and set `updated:`
2. Edit the to-be design document and set `updated:` to today. to today.
3. If the amendment came from an issue, set that issue's `amended-design:` to the document 3. If the amendment came from an issue, give the issue the document path in the issue tracker:
path. `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 ## When something ships
@@ -54,4 +120,17 @@ Implementation state is a third axis, independent of both design and decision.
- Do not move a to-be document into `00-as-is/`. Write the as-is document; both stand. - Do not move a to-be document into `00-as-is/`. Write the as-is document; both stand.
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for. - 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. - 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.
+91 -16
View File
@@ -8,40 +8,115 @@ design permits.
## What belongs here, and what does not ## 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 | | 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 documented rule is enforced by nothing | A command that works around it |
| A stated invariant is false in practice | A node-specific quirk | | 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 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 A fix's lesson (symptom → fix) is written where a search by the symptom's literal words finds it
folder is not a second copy of it. An issue here is a question HQ must **answer**, not an (`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**. 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 ## Steps
1. Take the next free number. 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 ```yaml
--- ---
status: open
opened: YYYY-MM-DD 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. Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
2. Investigate in `01-diagnosis.md` in the same folder — the trail, dated, including what was report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
ruled out. Move `status:` to `diagnosing`, then `located` once the owner is known. `replay:` or `replay-none:` in any report, because the tracker holds them and two places holding one fact
3. Resolve. Set `status: resolved`, fill `fixed-by:`, and if the root cause was a design gap, drift.
run playbook [02](02-graduation.md) and fill `amended-design:`.
**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 ## Rules
- Closed issues are never deleted — they are the mesh's symptom-to-component memory. - Closed issues are never deleted — they are the mesh's symptom-to-component memory.
- An issue whose answer is a general lesson should also be written to the knowledge base, so - `fixed-by` names something that will still exist: a commit or a pull request, never a branch. A
the next person searching a symptom finds it. Both, not either. branch is deleted when it merges, so a branch name there is a pointer that resolves to nothing by
- `status: wontfix` is legitimate and requires a sentence saying why. 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.
- 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 ## Steps
1. **Confirm the design is settled.** Its frontmatter reads `status: designed`, and every 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. 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 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 [`repos.md`](../repos.md). If the repository does not exist yet, add it to `repos.md` in
@@ -24,6 +25,10 @@
## Rules ## Rules
- Every merge is a human checkpoint, without exception. - 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 - Never open a pull request unprompted. A permissions list saying it is allowed is not a
request. request.
- Work in an isolated worktree, never a shared checkout. A failed `cd` in a shared checkout - Work in an isolated worktree, never a shared checkout. A failed `cd` in a shared checkout
+4 -1
View File
@@ -1,5 +1,8 @@
# Playbook 05 — Constitution sync # Playbook 05 — Constitution sync
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 **Trigger.** [`how-we-build.md`](../how-we-build.md) changed a rule that the mesh enforces at
runtime. runtime.
@@ -21,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. the derived page carries the rule alone.
2. Record the change as a decision — a rule the mesh enforces is architecturally significant. 2. Record the change as a decision — a rule the mesh enforces is architecturally significant.
Playbook [02](02-graduation.md). 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 body. Keep the section numbering stable: the meeting orchestrator and the review fragments
cite sections by number. cite sections by number.
4. Verify the derived page reads back with the change present. A publish that reported success 4. Verify the derived page reads back with the change present. A publish that reported success
+107
View File
@@ -0,0 +1,107 @@
# Playbook 06 — Writing a module
**Trigger.** Something that runs today must run on the mesh, or a new capability must be
declarable.
**Who runs it.** Whoever is porting or writing it.
*Written 2026-09-01 from doing this for the first time end to end. Every step below exists
because skipping it cost something.*
## Before anything: read what runs
**A module is written from the thing, not from memory of the thing.** For a port, that means its
current compose file, its environment, and where its data actually sits. Assumptions about any of
the three have been wrong every time they were not checked.
Three questions, answered from the machine:
| | why it decides something |
|---|---|
| **what containers, and how do they find each other?** | more than one means a `network`; names between them must match what the software is configured to dial |
| **where is its data?** | a bind mount moves with a path; a named volume does not; an anonymous volume is already losing data on every redeploy |
| **which values are secret, and which are merely settings?** | a secret goes in `own-secrets` or a grant; a setting goes in the manifest and may be overridden per node |
## 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):
`postgres-database`, not `database`. Most modules provide nothing and require nothing — an
application is usually a leaf.
2. **Declare capabilities, not dependencies, for facts about the machine.** `container-runtime`,
`package-manager`, `seat`. A capability is detected and refused against; it is not something a
module can install.
3. **Write the resources in the order they must happen.** They are applied in the order written
and orphans are removed in reverse, so a `network` is written before the containers that join
it and removed after them.
4. **Put every secret in a file, never in `env`.** A declaration travels over the broker in plain
text: a password in `env` is a password the broker sees. **The mesh delivers parts; a module
that needs them combined combines them.**
**A sealed file holds the password and nothing else** — no key, no `=`, no newline that means
anything. So `env-file` must never point at one. It points at a file the module *declares*,
whose content leaves a hole:
```
own-secrets superuser → /var/lib/postgres/superuser.secret the password, alone
a file /var/lib/postgres/superuser.env, mode 0600,
content: POSTGRES_PASSWORD=${secret:superuser}
the container env-file: [/var/lib/postgres/superuser.env]
```
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`
directly.
Every example module in `mesh-controller` had this wrong and shipped: `own-secrets` pointing at a
path *named* `.env`, mounted as `env-file`, holding a bare password. Docker reads that as a
malformed line and the container starts **with no password set at all** — not a failure to
start, a service running on the wrong credential. They parsed and they resolved. Two tests in
`examples/modules` now refuse both halves of it.
Add `restart-on` naming the env file, or the container keeps the credential it started with
through every rotation.
5. **Pin every image by digest.** A tag moves. The manifest in a repository names artifacts; the
manifest the mesh holds names digests, and they are not the same document.
6. **Decide generate or accept.** A new module's credential is generated. **An adopted one keeps
the credential it already has** — `secret accept` — because minting a new password for a
database that already exists locks the application out of its own data.
7. **Add a provisioner only if the software cannot read a file.** A proxy that watches a
directory needs nothing. PostgreSQL needs `CREATE ROLE`, an object store needs a bucket and a
policy, an identity provider needs a realm and a client — those need a small program beside
them. It reads what the mesh granted and reconciles; it does not decide anything.
8. **Prove it in the lab, against the real software.** Not that a container started — that the
thing works: the credential authenticates, a wrong one is refused, the containers reach each
other, the data survives a restart.
## What the first port actually cost
Six attempts, one real bug. Recorded because the ratio is the lesson: **the mesh was right every
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 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
and the suite stayed green.
- A test asserted on a helper rather than on the code that calls it, three separate times. **A
test that cannot fail when the behaviour is deleted is not defending the behaviour.**
## Rules
- **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),
and that protects against the mesh only — not against a disk or a mistaken command.
+69
View File
@@ -0,0 +1,69 @@
# Playbook 07 — Feature branches across repos
**Trigger.** Work that changes code — in one code repo or in several at once (`mesh-sdk`,
`mesh-controller`, `mesh-catalog`, `mesh-host`, `mesh-lab`, and `hq` when a decision rides along).
**Who runs it.** Anyone who writes code, engineers and agents alike. Agents follow it exactly —
it is the guard against the failure it was written for.
## The failure it prevents
A feature was worked as a branch-and-MR per *unit of thought* — one per decision, one per
stacked increment — and each MR was treated as finished when it was *opened*, not when it was
*merged*. Across repos the same feature took a different branch name in each. The MRs piled up
unmerged: one session left **sixteen** stacked intermediate MRs that had to be consolidated and
closed by hand. An MR is a review checkpoint, not a scratchpad.
## The rule
One feature is **one branch name**, **one worktree per repo**, **one MR per repo**, opened
**once, at the end**.
1. **Name the feature once.** `feat/<slug>`. The *same* branch name in every repo the feature
touches — never a different name per repo, never a fresh branch per increment within the
feature.
2. **Isolate each repo.** One git worktree per touched repo under `.work/<slug>/<repo>`, branched
off `main`:
```
git worktree add .work/<slug>/<repo> -b feat/<slug> origin/main
```
Parallel features never collide, and no shared checkout is edited.
**Add the untouched siblings the lab reads.** The lab beds find the other repositories by
sibling path from the lab checkout (`../mesh-tools/module.json`, `../mesh-sdk`, …), the way
the main layout has them. A `.work/<slug>/` directory holding only the touched repos fails a
bed at once with `no manifest for mesh-tools at …/.work/<slug>/mesh-tools/module.json`, after
genesis has already passed. Give the directory those repos as **detached worktrees on `main`**
— never symlinks:
```
git worktree add --detach .work/<slug>/mesh-tools main
```
3. **Commit as you go — locally.** Increments land on the one branch. Nothing is pushed and no
MR is opened mid-feature.
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); 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.
## What this is not
- **Not a licence to batch unbounded work.** A feature is a *bounded* unit; if it sprawls for
days, end-of-feature bloat merely replaces per-increment bloat. Split it into features, each
its own branch and MR.
- **Not a second trunk.** Every repo branches off `main`. There is no longer an `initialization`
trunk.
## How it is checked
The end state is visible, and its absence is the smell:
- After a feature merges, `git branch -r | grep feat/<slug>` and `git worktree list` return
nothing for it. A surviving branch or worktree means step 6 was skipped.
- A bed run from `.work/<slug>/mesh-lab` that fails naming a `.work/<slug>/<repo>/…` path it
cannot find is a missing sibling worktree, not a mesh fault.
- More than one open MR in a repo that share no feature name, or a stack of MRs none of which is
merged, is the failure this playbook exists to prevent — stop and consolidate before opening
more.
+16 -13
View File
@@ -1,6 +1,6 @@
--- ---
status: canonical status: canonical
updated: 2026-08-23 updated: 2026-09-24
--- ---
# The Novox repositories # The Novox repositories
@@ -15,27 +15,30 @@ and a forge address is an operational detail (see [`README`](../README.md)).
| Repository | Owns | | Repository | Owns |
|---|---| |---|---|
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. | | `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. | | *(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 ## 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-lab`, `mesh-host` and `mesh-control` exist so far** — the lab is built first monorepo decomposes into. **`mesh-host`, `mesh-controller`, `mesh-catalog`, `mesh-lab`, `mesh-sdk` and `mesh-tools` exist
([ADR 0016](../02-DECISIONS/0016-the-lab.md)); the rest are the so far** — the lab was built first (ADR 0016). The tiered
target, not the present. 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 | | 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-substrate` | 1 | the four pinned services, as declarations | | `mesh-foundation` | 1 | the four pinned services, as declarations |
| `mesh-control` | 2 | **exists.** The control plane 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-surfaces` | 3 | tools, web, cli |
| `mesh-sdk` | — | contracts shared across tiers | | `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. | | `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 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 ## What lives where inside the monorepo
@@ -53,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 ## Why applications do not live in the monorepo
A standalone application in the monorepo is a convention violation, and reviewers reject it. 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 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 whether or not its source sits beside the mesh's own — so co-location buys nothing and costs
the monorepo's review cadence. the monorepo's review cadence.
@@ -64,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 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 divergence — a workspace member importing another resolved to local unbuilt source in the
pipeline and to a published version in development. Recorded in 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 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. — 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,80 +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
- 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-substrate.md`](../../03-DESIGN/01-to-be/07-the-substrate.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-control-plane.md
- 03-DESIGN/01-to-be/07-the-substrate.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,199 +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
---
# 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.
## 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. |
| 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. |
+10 -44
View File
@@ -1,46 +1,12 @@
# 01-RESEARCH # 01-RESEARCH
Investigations that have not yet hardened into design. **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,
## Structure 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:
Each effort lives in `NNN-descriptive-name/` and **must** contain `00-overview.md`, carrying its `knowledge-base.ask` with a question in plain words, or `knowledge-base.get` with `kind: research` and a
state in YAML frontmatter and a prose summary below it: 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
```yaml [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,
status: active | graduated | abandoned the commit the knowledge base's records name as their source (issue 426).
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.
@@ -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`.
-84
View File
@@ -1,84 +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.
## 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: proposed
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
+23 -118
View File
@@ -1,123 +1,28 @@
# 02-DECISIONS # 02-DECISIONS
Architecture decision records — the "why" trail behind the rules in **The decision records live in the knowledge base, not here** (ADR 0290). The knowledge base is the module
[`00-META`](../00-META/) and the specifications in [`03-DESIGN`](../03-DESIGN/). `mesh-knowledge`, holding the seat `knowledge-base`; it holds every record whole — its number, title, topic,
status, date, deciders, its clauses, its typed links, its reasoning and every version — and it is the only
place a record 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, `knowledge-base.get` with a number (and
`reasoning: true` for the why), `knowledge-base.holds` with a topic below for the clauses in force. A record
is written with `knowledge-base.reserve` and `knowledge-base.propose`, as playbook
[02](../00-META/process/02-graduation.md) says. The records left this folder at hq commit 76b346af,
the commit the knowledge base's records name as their source (issue 426).
**Numbered `02` because a decision precedes the design it authorises.** Research concludes, ## Reading order
the decision is recorded here, and only then is the design written. Following the folder
numbers walks the process in the order it happens.
One file per decision, numbered, never deleted. A superseded record has its `status:` changed This folder keeps one thing: the six topics of the reading order, in the order somebody would learn the
and gains a pointer to what replaced it — **its text is never edited**. The reasoning that was system. Each record names its own topic, and the knowledge base reads these lines through the records module
rejected is the expensive half to rediscover. and refuses a record whose topic is not one of them. `index.py` checks that they are written.
The records run in the order the decisions were taken, oldest first. 1. **What the mesh is** — `topic: the mesh`. What it is for, and the parts it is made of.
2. **Its tiers, from the bottom up** — `topic: the tiers`. The foundation, the node and the controller, and
**Every decision is a record.** There is no ledger and no index file — if a decision is worth what each one provides to the one above.
recording it is worth a record, and if it is not worth a record it is not recorded 3. **What runs on them, and how it gets there** — `topic: what runs on it`. Modules, seats and
([ADR 0019](0019-how-this-repository-works.md)). A "decision" small enough to be one line is provisions, and how they reach a node.
almost always a **rule**, and a rule belongs in 4. **How it is built** — `topic: building it`. The code, its repositories, and what is made from
[`00-META/how-we-build.md`](../00-META/how-we-build.md), where it is enforced and keeps the it for a node.
incident that earned it. 5. **How it is checked** — `topic: checking it`. What judges a change, and what proves a decision holds.
6. **How we work** — `topic: how we work`. This repository, its playbooks, and how decisions and
## Frontmatter designs are made.
```yaml
---
status: proposed | accepted | superseded
date: YYYY-MM-DD # when the decision was taken, not when it was written down
deciders: name
reconstructed: true|false # true when the record was written after the fact from evidence
superseded-by: # 02-DECISIONS/NNNN-....md, when status is superseded
extends: # 02-DECISIONS/NNNN-....md, when this record widens an earlier one
---
```
## Body
```
# N. Title in plain language
## Context what was true, with evidence
## Considered Options numbered, each with why it was rejected
## Decision what was decided
## Consequences what follows, including what got harder
## References commits, pull requests, knowledge-base entries, prior art
```
State evidence, not assertion. *"Zero of 124 modules declare `brain` as a dependency"*
outranks *"the dependency rule is not followed"*.
## Reconstructed records
Records 0001–0014 were written on 2026-08-23, after the decisions they describe. Records 0015 onward were taken as records. Those
decisions were taken in implementation rather than in a document; the records state what was
decided and the evidence it was decided from, and each carries `reconstructed: true` and says
so in its first lines.
A reconstructed record is not a transcript. Where the deliberation is not recoverable, the
options section states what the alternatives were and why the chosen one won on the evidence
available — not a discussion that did not happen. Where a date is not establishable it says so
rather than guessing.
## Index
**A number identifies a record and never changes.** Records are referenced from outside this
repository — code comments, commit messages — so a number that moves invalidates them silently.
Renumbering once cost 96 references across two code repositories, and that is why the numbers
are now fixed.
So the folder is in creation order, and **the reading order lives here.** It is generated from
each record's `topic:` and written, because a reader looking at the folder on a forge sees the
folder rather than a command. The objection to a written index is that it drifts — which is
answered by checking it rather than by refusing to write one:
```
python3 00-META/checks/index.py --write regenerate
python3 00-META/checks/index.py fail if stale
```
<!-- index:start -->
### What the mesh is
- **0001** — [The mesh brokers capabilities; nodes host; agents think](0001-mesh-brokers-nodes-host-agents-think.md)
- **0002** — [Nodes communicate over a message broker, not over HTTP](0002-nodes-communicate-over-a-broker.md)
- **0003** — [An agent is a persistent employee, not an instance of a pool](0003-agents-are-persistent-employees.md)
### Its tiers, from the bottom up
- **0004** — [A node, and how it joins](0004-a-node-and-how-it-joins.md)
- **0005** — [The node host](0005-the-node-host.md)
- **0006** — [The substrate and the control plane](0006-the-substrate-and-the-control-plane.md)
- **0007** — [Connectivity](0007-connectivity.md)
- **0008** — [A context owns its store, exclusively](0008-a-context-owns-its-store.md)
### What runs on them, and how it gets there
- **0009** — [Modules and the graph](0009-modules-and-the-graph.md)
- **0010** — [Delivery](0010-delivery.md)
### How it is built
- **0011** — [Managed files are generated onto nodes and never edited there](0011-managed-files-are-generated-never-edited.md)
- **0012** — [The mesh creates no symlinks — a derived file is a copy](0012-the-mesh-creates-no-symlinks.md)
- **0013** — [Schema and state changes are numbered migrations, in the same language as the code](0013-schema-changes-are-numbered-migrations.md)
- **0014** — [No workspace — each module is a standalone package consuming published dependencies](0014-no-npm-workspace.md)
- **0015** — [Applications live in their own repository; the monorepo is for the mesh](0015-applications-live-in-their-own-repository.md)
- **0016** — [The lab](0016-the-lab.md)
### How it is checked
- **0017** — [A test defends a decision](0017-a-test-defends-a-decision.md)
- **0018** — [A picture of a system is read from the system, never from what asked for it](0018-a-picture-is-read-from-what-runs.md)
### How we work
- **0019** — [How this repository works](0019-how-this-repository-works.md)
- **0020** — [The mesh is governed by a constitution, injected where work is decided](0020-the-mesh-is-governed-by-a-constitution.md)
- **0021** — [HQ is the source of the mesh constitution](0021-hq-is-the-source-of-the-constitution.md)
- **0022** — [The constitution absorbs what is already enforced](0022-the-constitution-absorbs-what-is-enforced.md)
- **0023** — [The approval is the checkpoint, not the second pair of hands](0023-approval-is-the-checkpoint.md)
<!-- index:end -->
+12 -12
View File
@@ -4,9 +4,9 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0002-nodes-communicate-over-a-broker.md - "ADR 0002"
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md - "ADR 0006"
--- ---
# The mesh as it stands # The mesh as it stands
@@ -28,11 +28,11 @@ onto it and can be regenerated.
containerised service is a module. A set of capabilities with no service behind them is a containerised service is a module. A set of capabilities with no service behind them is a
module. A bare marker whose whole content is that a node has it is a module. The mesh's own module. A bare marker whose whole content is that a node has it is a module. The mesh's own
components are modules on exactly the same terms as everything else it carries components are modules on exactly the same terms as everything else it carries
([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)). (ADR 0009).
**An agent** is a participant. Some agents are human. What differs is modality — how the agent **An agent** is a participant. Some agents are human. What differs is modality — how the agent
acts — and not category: both hold identity, both act, both accumulate memory acts — and not category: both hold identity, both act, both accumulate memory
([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)). (ADR 0003).
## Where truth lives ## Where truth lives
@@ -40,10 +40,10 @@ The repository defines **what exists**: the modules, what each declares, how eac
The mesh database defines **what runs where**: which node is assigned which module, at which The mesh database defines **what runs where**: which node is assigned which module, at which
selection, with which overrides, plus the settings every node reads. No node-to-module mapping selection, with which overrides, plus the settings every node reads. No node-to-module mapping
is ever committed ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). is ever committed (ADR 0006).
Everything on a node's disk is **derived** from those two, and is regenerated rather than Everything on a node's disk is **derived** from those two, and is regenerated rather than
edited ([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)). A node that edited (ADR 0011). A node that
loses its database keeps running from a local cache, which is deliberate and has the obvious loses its database keeps running from a local cache, which is deliberate and has the obvious
cost: the cache carries no indication of its own age. cost: the cache carries no indication of its own age.
@@ -51,7 +51,7 @@ cost: the cache carries no indication of its own age.
Nothing dials a node. Every node dials the broker outbound, owns an exchange named for itself, Nothing dials a node. Every node dials the broker outbound, owns an exchange named for itself,
and consumes from its own request queue and consumes from its own request queue
([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)). Three message shapes carry (ADR 0002). Three message shapes carry
everything: requests expecting a reply, commands instructing that a stage of work be done, and everything: requests expecting a reply, commands instructing that a stage of work be done, and
events stating that something happened. events stating that something happened.
@@ -65,9 +65,9 @@ goes to where the capability is.
A push to the forge is the only trigger. What follows is three silos with deliberately A push to the forge is the only trigger. What follows is three silos with deliberately
different cardinality: compile once, package and upload once, then install-configure-start- different cardinality: compile once, package and upload once, then install-configure-start-
verify **on every assigned node** verify **on every assigned node**
([ADR 0010](../../02-DECISIONS/0010-delivery.md)). What travels between (ADR 0010). What travels between
build and node is a self-contained build output, so a deploy is extract-and-run and touches no build and node is a self-contained build output, so a deploy is extract-and-run and touches no
network ([ADR 0010](../../02-DECISIONS/0010-delivery.md)). network (ADR 0010).
Modules are resolved into dependency levels and a level completes before the next begins, so a Modules are resolved into dependency levels and a level completes before the next begins, so a
module always builds against its dependencies as they were just published. module always builds against its dependencies as they were just published.
@@ -78,7 +78,7 @@ A module declares what it **provides** and what it **requires**. The mesh satisf
requirement: it creates the resource, generates the credential, records the grant, and writes requirement: it creates the resource, generates the credential, records the grant, and writes
the values where the module will read them. The module never learns which node its database the values where the module will read them. The module never learns which node its database
lives on, and nobody ever writes a credential by hand lives on, and nobody ever writes a credential by hand
([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)). (ADR 0009).
This is the property the mesh's whole shape rests on, and it is why provisioning is treated as This is the property the mesh's whole shape rests on, and it is why provisioning is treated as
a core concern rather than as plumbing. a core concern rather than as plumbing.
@@ -92,7 +92,7 @@ named for a feature the module does not declare, a stage that reported it had di
message rather than that the effect happened, a package that 404ed from every mirror while the message rather than that the effect happened, a package that 404ed from every mirror while the
job went green. job went green.
[ADR 0010](../../02-DECISIONS/0010-delivery.md) is the response, and it is applied ADR 0010 is the response, and it is applied
instance by instance rather than enforced by a mechanism. New instances are still being found. instance by instance rather than enforced by a mechanism. New instances are still being found.
That is an as-is fact, not a criticism: it is the single most useful thing to know about this That is an as-is fact, not a criticism: it is the single most useful thing to know about this
system before changing it. system before changing it.
+6 -6
View File
@@ -4,8 +4,8 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0002-nodes-communicate-over-a-broker.md - "ADR 0002"
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md - "ADR 0006"
--- ---
# The mesh and its transport # The mesh and its transport
@@ -20,7 +20,7 @@ One relational database holds the bindings. Its content divides cleanly:
| Holds | Describes | | Holds | Describes |
|---|---| |---|---|
| Node records | Which nodes exist, and each node's own properties — its name in the mesh, whether it carries a public name, its identity text | | Node records | Which nodes exist, and each node's own properties — its name in the mesh, whether it carries a public name, its identity text |
| Assignments | Which node hosts which module, at which selection, and whether it starts automatically | | Assignments | Which node runs which module, at which selection, and whether it starts automatically |
| Overrides | Per-node, per-module values that take precedence over anything the manifest generates | | Overrides | Per-node, per-module values that take precedence over anything the manifest generates |
| Mesh settings | Values every node reads — where the broker is, where the forge is, where the registry is | | Mesh settings | Values every node reads — where the broker is, where the forge is, where the registry is |
| Grants | Which consumer holds which resource from which provider, with the credential | | Grants | Which consumer holds which resource from which provider, with the credential |
@@ -87,7 +87,7 @@ down.
## Reaching a capability on another node ## Reaching a capability on another node
A node hosts some capabilities and can reach the rest. A node holds some capabilities and can reach the rest.
At startup, a node asks every peer what it hosts. For anything hosted elsewhere it creates a At startup, a node asks every peer what it hosts. For anything hosted elsewhere it creates a
local stand-in that forwards over the broker. For anything hosted both locally and elsewhere local stand-in that forwards over the broker. For anything hosted both locally and elsewhere
@@ -103,8 +103,8 @@ restarts. Nothing re-discovers on a schedule.
## Names and reachability ## Names and reachability
Nodes address each other by names that resolve on the mesh's own overlay, not on whatever the Nodes address each other by names that resolve on the mesh's own private network, not on whatever the
underlying network provides. A node's mesh name is its overlay address; its public name, if it underlying network provides. A node's mesh name is its private network address; its public name, if it
has one, is a separate fact used by things outside the mesh. has one, is a separate fact used by things outside the mesh.
Two lessons are embedded in that separation, both learned the expensive way. A name resolved Two lessons are embedded in that separation, both learned the expensive way. A name resolved
@@ -4,9 +4,9 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
- 02-DECISIONS/0013-schema-changes-are-numbered-migrations.md - "ADR 0013"
- 02-DECISIONS/0014-no-npm-workspace.md - "ADR 0014"
--- ---
# Modules, manifests and features # Modules, manifests and features
@@ -81,7 +81,7 @@ recorded in the knowledge base; both presented as "the change did not apply" wit
## Dependencies between modules ## Dependencies between modules
Modules depend on each other, above all on the shared library they all build against. There is Modules depend on each other, above all on the shared library they all build against. There is
**no workspace** ([ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md)): each module is a standalone **no workspace** (ADR 0014): each module is a standalone
package consuming published dependencies, including the mesh's own. package consuming published dependencies, including the mesh's own.
The pipeline resolves modules into dependency **levels** and completes a level before starting The pipeline resolves modules into dependency **levels** and completes a level before starting
@@ -96,7 +96,7 @@ since (see
A module that owns state owns its migrations: numbered, written in the module's own language, A module that owns state owns its migrations: numbered, written in the module's own language,
compiled with it, frozen once they have run anywhere, and idempotent so that re-running is safe compiled with it, frozen once they have run anywhere, and idempotent so that re-running is safe
([ADR 0013](../../02-DECISIONS/0013-schema-changes-are-numbered-migrations.md)). (ADR 0013).
Two kinds exist and the distinction matters: migrations against the module's **own** local Two kinds exist and the distinction matters: migrations against the module's **own** local
state, and migrations against a **provisioned** resource, which run on the node that consumes state, and migrations against a **provisioned** resource, which run on the node that consumes
+2 -2
View File
@@ -4,8 +4,8 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md - "ADR 0011"
--- ---
# Provisioning # Provisioning
+5 -5
View File
@@ -4,9 +4,9 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0010-delivery.md - "ADR 0010"
- 02-DECISIONS/0010-delivery.md - "ADR 0010"
- 02-DECISIONS/0010-delivery.md - "ADR 0010"
--- ---
# Delivery — from a push to a running node # Delivery — from a push to a running node
@@ -31,7 +31,7 @@ merge that created no pipeline, and nothing said so**.
## Three silos ## Three silos
Cardinality is the whole point, and the three differ Cardinality is the whole point, and the three differ
([ADR 0010](../../02-DECISIONS/0010-delivery.md)): (ADR 0010):
| Silo | Runs | Where | Does | | Silo | Runs | Where | Does |
|---|---|---|---| |---|---|---|---|
@@ -50,7 +50,7 @@ later stage runs.
## The artifact ## The artifact
The artifact is **build output** — compiled and bundled with its dependency graph inlined — The artifact is **build output** — compiled and bundled with its dependency graph inlined —
never a filtered copy of source ([ADR 0010](../../02-DECISIONS/0010-delivery.md)). never a filtered copy of source (ADR 0010).
A deploy is extract-and-run and touches no network. 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 The consequence is the whole cost of the decision: **anything not in the build output does not
@@ -4,8 +4,8 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
- 02-DECISIONS/0012-the-mesh-creates-no-symlinks.md - "ADR 0012"
--- ---
# The node runtime, and how a node comes into being # The node runtime, and how a node comes into being
@@ -38,7 +38,7 @@ suggestive word in the system names the node runtime, and the component whose ma
"mesh messaging" is documented elsewhere as the interactive runtime. Anatomy makes attractive "mesh messaging" is documented elsewhere as the interactive runtime. Anatomy makes attractive
names and poor boundaries. names and poor boundaries.
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) replaces this with names ADR 0001 replaces this with names
taken from what each part owns. Until then, this is the vocabulary in the code. taken from what each part owns. Until then, this is the vocabulary in the code.
## Starting a module ## Starting a module
@@ -56,20 +56,19 @@ reconcile the link from the catalogue's definition into it, generate the environ
outstanding local migrations, create data directories with the right ownership, then start the outstanding local migrations, create data directories with the right ownership, then start the
service under supervision. service under supervision.
**The installer is the only thing that creates a link** ([ADR **The installer is the only thing that creates a link** (ADR 0012). It reconciles rather than assumes: a
0011](../../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)). It reconciles rather than assumes: a
missing link is created, a stale one repointed, and a real file found where a link belongs is missing link is created, a stale one repointed, and a real file found where a link belongs is
adopted into the node's override area and replaced. Nothing else — not a hook, not a fix, not a adopted into the node's override area and replaced. Nothing else — not a hook, not a fix, not a
person debugging — creates one. person debugging — creates one.
That is the as-is. The intent is to remove linking altogether and derive a real file instead, That is the as-is. The intent is to remove linking altogether and derive a real file instead,
which the reconciliation machinery already makes possible which the reconciliation machinery already makes possible
([ADR 0012](../../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md), proposed). What is described above (ADR 0012, proposed). What is described above
is what runs today. is what runs today.
## Supervision ## Supervision
Services run under the host's init system via a templated unit, one instance per module. It is Services run under the machine's init system via a templated unit, one instance per module. It is
a thin layer: the unit starts and stops a container group. a thin layer: the unit starts and stops a container group.
Whether the mesh keeps this, drops the per-module layer, containerises the daemons, or writes Whether the mesh keeps this, drops the per-module layer, containerises the daemons, or writes
@@ -1,11 +1,12 @@
--- ---
layer: as-is layer: as-is
status: implemented status: implemented
code: [hal] code: [hal, mesh-controller, mesh-catalog, mesh-host]
updated: 2026-08-23 updated: 2026-09-21
decisions: decisions:
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md - "ADR 0011"
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
- "ADR 0085"
--- ---
# Configuration and secrets # Configuration and secrets
@@ -17,7 +18,7 @@ files is **generated**.
A managed file is derived from the mesh database. A synchroniser rewrites it when the values A managed file is derived from the mesh database. A synchroniser rewrites it when the values
behind it change. The write path is the mesh operation that owns the value; the file is an behind it change. The write path is the mesh operation that owns the value; the file is an
output ([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)). output (ADR 0011).
An edit to a managed file survives until the next synchronisation and is then overwritten An edit to a managed file survives until the next synchronisation and is then overwritten
silently, taking whatever it was fixing with it — bringing back the bug the edit had removed, silently, taking whatever it was fixing with it — bringing back the bug the edit had removed,
@@ -61,7 +62,7 @@ are both left behind. Configuration is additive in practice, whatever the manife
Generated secrets are produced by the mesh, never authored. Provisioned credentials arrive as Generated secrets are produced by the mesh, never authored. Provisioned credentials arrive as
database overrides written by the provisioner and are marked as such, so they can be database overrides written by the provisioner and are marked as such, so they can be
distinguished from a deliberate override and cleaned up when the grant is removed distinguished from a deliberate override and cleaned up when the grant is removed
([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)). (ADR 0009).
Nothing in the repository contains a credential. The repository has no per-node content at all, Nothing in the repository contains a credential. The repository has no per-node content at all,
which is what makes that guarantee structural rather than a matter of care. which is what makes that guarantee structural rather than a matter of care.
@@ -73,9 +74,31 @@ when the file is created, so regeneration left the previous mode in place. The c
worth remembering beyond the instance: a permission set at creation is not a permission worth remembering beyond the instance: a permission set at creation is not a permission
maintained. maintained.
**Rotation is not a mesh operation.** Secrets can be generated and granted; there is no **Rotation was not a mesh operation** in the mesh being replaced, and doing it by hand took
mechanism that rotates one and informs everything holding it. Where a rotation has been done, services down. On the mesh that exists now it is: `rotate <provision>` discards a pair credential
it has been done by hand, and doing it wrong has taken services down. and delivers both ends in one send, and a module's own secret is such a pair credential when the
module takes it from the vault — which, at the time of writing, one module does (redis).
## The vault, as it runs
Since 2026-09-21 the mesh runs `mesh-vault`, a foundation module installed at genesis beside the
adopted store and broker. It provides `secret`: a module that requires one receives a pair
credential the controller minted, and the vault's ledger records the holder and the value's
fingerprint, notices a rotation, and answers over the mesh by fingerprint only. It holds no value.
Genesis makes the store's superuser and the broker's administrator rather than copying the
template's, keeps them at the paths the store and broker modules declare as their own secrets, and
makes an **operator sealing key** before the first secret is accepted: its private half is a file
beside the produced bundle, which the operator carries off the machine, and its public half is
what the mesh records. Every secret a module holds for itself and every pair credential is sealed
to that key as well as to its node. The export of those copies is written beside the key at the
end of genesis and kept by the vault on its own disk; the operator recovers any secret from it, off
the mesh, with `secret recover`. Secrets made before the key existed, or sealed to a replaced key,
are listed as such rather than passed off as recoverable. The produced bundle and the installer's
transcript carry no credential in the clear.
Two things this does not yet do: a module with several own secrets cannot take them all from the
vault (issue 069), and an operator cannot hand a value into a pair credential (issue 070).
## Node-level and mesh-level values ## Node-level and mesh-level values
+40 -60
View File
@@ -1,78 +1,58 @@
--- ---
layer: as-is layer: as-is
status: implemented status: implemented
code: [hal] code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
updated: 2026-08-23 updated: 2026-09-30
decisions: [] decisions:
- "ADR 0025"
- "ADR 0153"
- "ADR 0152"
--- ---
# Knowledge # Knowledge
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the **The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
difference between finding an answer in one search and rediscovering it over several hours. or an agent asks is the mesh MCP server's tool list on the machine they sit at
([13 — The mesh MCP server](13-the-console.md)). This document used to describe two stores; it is rewritten
because neither exists from the mesh's side, and an as-is document that describes what is gone is a
brochure.
## The operational memory ## What was here, and where it went
A store of operational notes, written and read by whoever — human or agent — is working. Each Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
note is a slug and a body: how something works, what went wrong, what the fix was, what indexed on symptoms, and a structured archive of governed documents with a librarian approving
assumption turned out to be false. promotion. Both were reached through the predecessor's tool server over the bus the mesh removed
([issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
Nothing in the mesh reaches them now, and nothing in the mesh has replaced them: there is no note
store, no archive, no librarian, and the lessons of the last days were written into this repository by
hand. That is a gap, and it is stated here rather than papered over. What replaces a symptom-indexed
memory, if anything does, is undecided.
It is indexed on **symptoms**. The entry someone needs is usually titled after the error they ## The record
are staring at, which is why the standing instruction is to search the literal error text
before forming a hypothesis rather than after one fails.
Its content is overwhelmingly the record of previous debugging: a large body of **The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
troubleshooting entries, module conventions, and standing notes about work that is open. It is checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
the mesh's institutional memory of *what has already gone wrong*. merge the forge announces and every ten minutes — and answers over the bus: where a phrase appears as
written, one document whole, what a folder holds, and where the checkout stands, each naming the
commit it read. Which repository it reads is a setting on its assignment; the module names no mesh.
The cost of skipping it is documented in the mesh's own record: entries have been rediscovered It is listed by the mesh MCP server beside every other tool, with a description that says to search the
from scratch, over hours, in sessions where the search was skipped because the trail felt literal words of a symptom before forming a hypothesis. That is what
confident. It fires hardest on familiar ground, not unfamiliar ground. ADR 0025 meant by *beside
everything else*, in a mesh with no store to be beside
(ADR 0153).
## The structured archive Nothing is copied. A checkout lags the source by seconds after an announced merge and by minutes
otherwise, and says so.
A second store, structured rather than flat: spaces, pages, revisions, tiers, and full-text ## The constitution
search. Where the operational memory is a note, this is a document with an owner and a
lifecycle.
Content is promoted through tiers — private, then team, then platform — with a librarian agent [`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than (ADR 0021). The page it used to be
applied. synchronised into lived in the predecessor's archive and is unreachable; the constitution today is
read from this repository, through the same module, and playbook 05's sync has nothing to write to.
This is where the mesh's **governed** documents live, including the constitution injected into
design sessions ([ADR 0020](../../02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md)).
## Why both
The distinction is by lifecycle, not by subject.
| Operational memory | Structured archive |
|---|---|
| Written the moment something is learned | Written deliberately, reviewed |
| Flat, symptom-indexed | Structured, tiered, owned |
| Anyone writes; nothing approves | Promotion is approved |
| Truth is "this happened" | Truth is "this is agreed" |
Collapsing them would cost one of the two properties: either every hard-won note waits for
review, or governed documents can be changed by anyone mid-incident.
## Where this repository sits ## Where this repository sits
This repository is a third thing, and the objection was raised when it was created: a fourth A third thing beside two that are gone, which makes it the first: the one governed record the mesh
knowledge system repeats the mistake the split was made to fix. has, public, read by a module the mesh assigns, and edited nowhere else.
The answer given was **indexing, not location** — that these documents are indexed into the
knowledge base so that a symptom search returns them alongside everything else. One source,
many surfaces.
**That indexing does not currently exist.** A search for this repository's content returns
nothing. The claim is load-bearing for the decision to separate the repository at all, and
until it is true, this repository is exactly the fourth knowledge system the objection
described. Recorded here because it is a statement about how the mesh's knowledge actually
works today, and as [`04-ISSUES/006`](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md).
## The librarian
A single agent owns the archive's approvals and promotions. Its approval capabilities have at
times not been reachable as tools, which does not affect the operational memory but does mean
promotion stops silently — the store keeps accepting proposals that nothing can approve.
+6 -6
View File
@@ -4,8 +4,8 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0003-agents-are-persistent-employees.md - "ADR 0003"
- 02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md - "ADR 0020"
--- ---
# Agents and work # Agents and work
@@ -17,7 +17,7 @@ model they run under is the employee model, not a worker pool.
An agent is a singular named identity with a home node, a workspace on that node, accumulating An agent is a singular named identity with a home node, a workspace on that node, accumulating
memory, and an explicit lifecycle memory, and an explicit lifecycle
([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)). (ADR 0003).
| Property | Meaning | | Property | Meaning |
|---|---| |---|---|
@@ -45,7 +45,7 @@ Both hold identity, both act, both accumulate memory.
The mesh does not currently record modality completely. Which user, on which node, a human The mesh does not currently record modality completely. Which user, on which node, a human
agent acts as is **required by the model and not stored** — an open question carried over from agent acts as is **required by the model and not stored** — an open question carried over from
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md). ADR 0001.
## Work ## Work
@@ -70,7 +70,7 @@ template that names the phases.
This is where governance meets execution. The constitution is injected into every eligible This is where governance meets execution. The constitution is injected into every eligible
meeting turn — agents do not fetch it, it arrives — and a check phase verifies the meeting's meeting turn — agents do not fetch it, it arrives — and a check phase verifies the meeting's
output against it before the meeting may proceed output against it before the meeting may proceed
([ADR 0020](../../02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md)). A named violation (ADR 0020). A named violation
blocks progress. blocks progress.
Meeting turns run on the orchestrator's node regardless of where the participating agents are Meeting turns run on the orchestrator's node regardless of where the participating agents are
@@ -84,5 +84,5 @@ integrate through the record, never through a shared schema* — being violated
own largest component, and it is the reason work that belongs to one domain keeps having to be own largest component, and it is the reason work that belongs to one domain keeps having to be
implemented in another. implemented in another.
[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) dissolves that arrangement. ADR 0001 dissolves that arrangement.
Until it does, this is the shape. Until it does, this is the shape.
@@ -4,8 +4,8 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0002-nodes-communicate-over-a-broker.md - "ADR 0002"
- 02-DECISIONS/0010-delivery.md - "ADR 0010"
--- ---
# Interfaces and observability # Interfaces and observability
@@ -14,7 +14,7 @@ How the mesh is reached, and how anyone can tell what it is doing.
## Capabilities are the primary interface ## Capabilities are the primary interface
The mesh's primary interface is not a web console. It is a set of **capabilities**, exposed to The mesh's primary interface is not a web dashboard. It is a set of **capabilities**, exposed to
a session and callable in language. a session and callable in language.
A capability is contributed by a module and is available on any node, wherever it actually A capability is contributed by a module and is available on any node, wherever it actually
+10 -10
View File
@@ -4,16 +4,16 @@ status: implemented
code: [hal] code: [hal]
updated: 2026-08-23 updated: 2026-08-23
decisions: decisions:
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
- 02-DECISIONS/0015-applications-live-in-their-own-repository.md - "ADR 0015"
- 02-DECISIONS/0009-modules-and-the-graph.md - "ADR 0009"
--- ---
# The catalogue, and what its shape says # The catalogue, and what its shape says
The catalogue holds **124 modules**. Thirty-three belong to the mesh's own domain; the other The catalogue holds **124 modules**. Thirty-three belong to the mesh's own domain; the other
ninety-one run *on* the mesh rather than being *of* it ninety-one run *on* the mesh rather than being *of* it
([ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md)). (ADR 0001).
The count is not the finding. The **shape** is. The count is not the finding. The **shape** is.
@@ -55,11 +55,11 @@ connectivity is made four times.
the unit of one piece of software, because that is the only granularity the module system the unit of one piece of software, because that is the only granularity the module system
offers. offers.
This is the same failure [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) This is the same failure ADR 0001
names for the platform core — *boundaries drawn by deployment accident rather than by domain* — names for the platform core — "boundaries drawn by deployment accident rather than by domain" —
appearing outside it, at four times the scale. The core is being recomposed; the flat level is appearing outside it, at four times the scale. The core is being recomposed; the flat level is
addressed in principle by addressed in principle by
[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), which ADR 0009, which
deliberately does not yet settle the domain list. deliberately does not yet settle the domain list.
## Where the shape came from ## Where the shape came from
@@ -90,11 +90,11 @@ never stated as assumptions — they were just how the thing already worked.
**This is the most useful single fact for anyone changing the catalogue**, and it is why the **This is the most useful single fact for anyone changing the catalogue**, and it is why the
linking principle in particular reads as a deliberate architectural choice when it is an linking principle in particular reads as a deliberate architectural choice when it is an
inheritance. See [ADR 0012](../../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md), whose case inheritance. See ADR 0012, whose case
this strengthens: the argument for links was never made *for a mesh*. this strengthens: the argument for links was never made *for a mesh*.
It also explains the measurement in It also explains the measurement in
[research 005](../../01-RESEARCH/005-domain-grouping/analysis.md). Fifty modules that never research 005. Fifty modules that never
change alongside anything are not fifty missing domains — many are dotfiles-era entries for one change alongside anything are not fifty missing domains — many are dotfiles-era entries for one
piece of software, which never shared a domain because they never had one. piece of software, which never shared a domain because they never had one.
@@ -108,7 +108,7 @@ which is what makes dogfooding structural rather than a discipline, and what mak
module out of the repository safe. module out of the repository safe.
**Placement is already decided.** A standalone application belongs in its own repository **Placement is already decided.** A standalone application belongs in its own repository
([ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md)), and reviewers reject (ADR 0015), and reviewers reject
it in the monorepo. The catalogue's flat level is not a dumping ground by policy; it is one by it in the monorepo. The catalogue's flat level is not a dumping ground by policy; it is one by
history. history.
+10 -10
View File
@@ -4,11 +4,11 @@ status: implemented
code: [mesh-lab] code: [mesh-lab]
updated: 2026-08-25 updated: 2026-08-25
decisions: decisions:
- 02-DECISIONS/0016-the-lab.md - "ADR 0016"
- 02-DECISIONS/0016-the-lab.md - "ADR 0016"
- 02-DECISIONS/0016-the-lab.md - "ADR 0016"
- 02-DECISIONS/0016-the-lab.md - "ADR 0016"
- 02-DECISIONS/0016-the-lab.md - "ADR 0016"
--- ---
# The lab, as it stands # The lab, as it stands
@@ -46,7 +46,7 @@ scenario as one state.
## What it does not do, and why that matters ## What it does not do, and why that matters
**`place:` is refused.** A scenario can declare that a node host is placed on a machine; the **`place:` is refused.** A scenario can declare that a node-engine is placed on a machine; the
lab names the gap and refuses rather than raising a scenario that silently lacks what it lab names the gap and refuses rather than raising a scenario that silently lacks what it
declared. Nothing can be placed because tier 0 does not exist yet. declared. Nothing can be placed because tier 0 does not exist yet.
@@ -63,7 +63,7 @@ is built.
**The drawing was never designed.** `diagram` renders a scenario as draw.io, from the **The drawing was never designed.** `diagram` renders a scenario as draw.io, from the
declaration or from the running instance, and it exists because it was asked for during the declaration or from the running instance, and it exists because it was asked for during the
build. It has tests and a decision record ([ADR 0018](../../02-DECISIONS/0018-a-picture-is-read-from-what-runs.md), build. It has tests and a decision record (ADR 0018,
proposed) but no document in the to-be layer. It is recorded here because it runs, not because proposed) but no document in the to-be layer. It is recorded here because it runs, not because
it was planned. it was planned.
@@ -117,7 +117,7 @@ and snapshots roughly 76× slower, which does not make the lab slow, it makes it
`npm run check` — typecheck over source *and* tests, then the offline suite, then integration `npm run check` — typecheck over source *and* tests, then the offline suite, then integration
against a real hypervisor. Mocking the hypervisor is forbidden against a real hypervisor. Mocking the hypervisor is forbidden
([ADR 0017](../../02-DECISIONS/0017-a-test-defends-a-decision.md), proposed): a test that fakes (ADR 0017, proposed): a test that fakes
the system under integration asserts that the fake behaves as expected. the system under integration asserts that the fake behaves as expected.
Integration tests **skip with a reason** on a machine that cannot raise scenarios, rather than Integration tests **skip with a reason** on a machine that cannot raise scenarios, rather than
@@ -139,7 +139,7 @@ Two things the suite does not yet do, recorded because their absence is invisibl
to one. to one.
- [`01-to-be/04-lab-installation.md`](../01-to-be/04-lab-installation.md) — what the - [`01-to-be/04-lab-installation.md`](../01-to-be/04-lab-installation.md) — what the
workstation needs. workstation needs.
- [Research 004](../../01-RESEARCH/004-lab-network/00-overview.md) — the topology, and the - Research 004 — the topology, and the
address-range constraint the whole thing turns on. address-range constraint the whole thing turns on.
- [Research 010](../../01-RESEARCH/010-lab-inner-loop-cost/00-overview.md) — where the measured - Research 010 — where the measured
costs come from. costs come from.
+107
View File
@@ -0,0 +1,107 @@
---
layer: as-is
status: implemented
code: [mesh-controller, mesh-catalog]
updated: 2026-09-26
decisions:
- "ADR 0109"
- "ADR 0110"
- "ADR 0111"
---
# The seats, as they run
Written from the controller's code and the catalogue's manifests on their main branches. This is
the second piece of the new shape to exist, after [the lab](11-the-lab.md), and the first that the
predecessor has no equivalent of: there, any well-formed name became a seat by being claimed.
## The set is closed, and it lives in the controller
Fourteen seats are defined in the controller — seven at mesh scope, seven at node scope. Each entry
carries a name, the scope at which there may be only one holder, the provision its holder answers
for (or none), and the record that made it a seat. A person reads the set to learn what a mesh can
have; nothing else can add to it.
Five seats deliver a provision: the mesh's store delivers the relational database, the mesh's broker
the message transport, the artifact-store seat the registry, and two more the package registry for
one ecosystem and the git service. The remaining nine — the controller and catalogue seats, and the
node-scope ones for the builder, the DNS port, the packet filter, intrusion prevention, the
private network, the resolver's configuration and the showcase — mark a role without answering for
anything a consumer requires.
## A claim is checked against the set
Parsing a manifest refuses three things, in this order: a claim that is not a usable name or not at
node, site or mesh scope; then a claim naming a seat the mesh does not define, answered with the
whole set so the writer can see what was available; then a claim at the wrong scope for that seat.
A malformed claim is refused once, for being malformed, and not a second time for being unknown.
One further refusal ties a seat to what it promises: claiming a seat that delivers a provision is
refused unless the claiming module actually provides that provision, at the seat's scope. A seat
cannot be held by something that could not answer for it.
## A seat is held by an assignment, and nothing else is recorded
The holder is a module assignment — a node and a module together. There is no separate record of
holders: what the mesh knows about one is what it already knows about that assignment. The pair is
also what tells a seat's holder apart from another module providing the same thing on another node.
## Where a seat changes resolution, and where it does not
Resolution runs in two passes. The first learns only what each node offers and takes requirements
on trust; no declaration is ever built from it. In the second:
- **nothing provides the wanted provision** — refused, naming what would answer it ("assign X to a
node");
- **one provider** — taken, unless a pin names a different node, which is refused rather than
silently overruled;
- **several providers** — a pin wins; failing that, the holder of the seat that delivers the
provision answers; failing that, refused with the candidates listed.
So a seat decides **which** of several providers answers. It does not make a requirement optional:
before the seat is held, a requirement for what it delivers is refused like any other unanswerable
one. That is the standing condition [issue 121](../../04-ISSUES/121-builders-real-package-registry-grant-deadlocks-genesis/00-report.md)
records, where the module that builds the forge's image requires a registry only the forge provides.
## The mesh can be asked
A `seats` command prints every seat with its scope, what it delivers and who holds it, in reading
order, and the same as JSON. An unheld seat prints as an answer — this mesh has no such thing — not
as a fault. Claims held that are **outside** the set are listed separately rather than hidden, so a
mesh carrying one from before the set existed says so.
## A build source may name the git seat
A module's repository is either a URL, recorded and cloned exactly as given, or a path on the forge
holding the git seat, recorded as that path plus the seat. The clone URL is composed from wherever
the holder runs at the moment of building, so the builder is never told an address that could
go stale. Before this, a self-hosted forge's scheme, host and port were written into every module
built from it, and moving the forge made every record stale at once — noticed when a rebuild failed
to clone.
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
real answer, so every row recorded before the change keeps exactly the meaning it had.
## A seat's protocol is on its row, and the controller serves its own
*Since 2026-09-30 (ADR 0154).*
The `seat` table carries `accepts`, `emits` and `serves`; `serves` holds each verb with its description
and input schema. The rows were seeded from the compiled defaults the first time a controller with the
columns migrated, and each later migration adds any verb the defaults name that a row lacks, never
removing one. `UseSeats` still falls back to the compiled protocol for a row with none, which after the
first seeding is no row.
The `mesh-controller` seat serves twelve verbs — `tools`, `status`, `nodes`, `node`, `modules`, `seats`,
`builds`, `plan`, `assign`, `unassign`, `push`, `build` — on `mesh.seat.mesh-controller.tool.<verb>`,
each answered by the controller running that command in its own binary and returning what it printed.
A module claiming a mesh seat with verbs must list them under `tools` or registration refuses it by
name. A node-scoped seat's tool is `mesh.seat.<seat>.tool.<verb>.<node>`; no node-scoped seat declares
one yet.
## Where this differs from the design
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
higher-capacity seat is a bench several holders share. Neither exists in the code: a seat as it runs
is one role with one holder at its scope, and nothing expresses a bench. Every seat in the set today
is exclusive, so nothing has yet needed it — but a design that says capacity and an implementation
that has none is a disagreement worth reading here rather than discovering in the type.
+69
View File
@@ -0,0 +1,69 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
updated: 2026-10-10
decisions:
- "ADR 0152"
- "ADR 0095"
- "ADR 0037"
- "ADR 0154"
---
# The mesh MCP server, as it runs
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
`<node>.mesh-console`, seals its credential to the machine, and the container binds
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
endpoint. Nothing on the machine holds a credential a person had to carry.
## What it answers
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
stream. `tools/list` is what the running modules answered: every tool runner built on or after that day
serves a `tools` verb for its module, and the mesh MCP server asks the catalogue for the roster and each module
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
mesh records rather than rolls out — and 62 tools from the rest.
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The mesh MCP server's grant is `*`, so what it
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
No call is refused for want of a person's approval: a tool runner serves every call it is issued, whoever made
it, and cannot tell an agent from the operator (ADR 0259 §6, issue 340). An act waits for the operator's warrant
only where its module asks first, in its own code: the agent module's instruction proposals, and the
controller's settings proposals and the actions it offers on a condition.
## The mesh's own verbs
*Since 2026-09-30 evening (ADR 0154).*
The mesh MCP server asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
otherwise; `seat:<seat>.<verb>` says so outright. When the controller does not answer, the list
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
## Around it
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
person's account is; the mesh MCP server is the only module that declares it.
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
judge without a store rather than refusing. The mesh MCP server's own manifest was the first thing checked
with it, and the whole catalogue passes.
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
`mesh tools --console <url>` goes through a running mesh MCP server with no credential; it is covered by the
runtime repository's tests and was not exercised on the live mesh.
## What shipped bent
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
operator did not pass. The rebuild-on-merge matched the URL anyway.
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
something pushed their rebuilt runtime; until then they are listed as not answering while still
callable. That is the policy doing what it says, not a fault of the mesh MCP server.
+51
View File
@@ -0,0 +1,51 @@
---
layer: as-is
status: implemented
code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/broker/state.go, mesh-controller cmd/mesh-controller/push.go, mesh-tools node-tools/internal/bus/state.go, mesh-tools node-tools/internal/launch/launch.go, mesh-sdk src/state, mesh-sdk go/state.go]
updated: 2026-10-04
decisions:
- "ADR 0201"
---
# A module's state, as it runs
**A module keeps the current value of something on the bus, and every machine sees it — including one
that joins later.** Since 2026-10-04 a manifest may say `state` (buckets the module owns) and `reads`
(another module's, as `<module>.<name>`). The first two modules to use it are the operator's agent on a
machine and its licence manager; on the day this was written, three buckets existed on the bus.
## What runs
- **The controller** creates a key-value bucket `<module>_<name>` for every declared state, from the
catalogue — on every start and, since the first module that declared state found it missing, on every
push before the memberships that name it. A bucket nothing declares any more is reported and kept.
Every bucket carries the mesh's caps: 256 KiB a value, 64 MiB a bucket.
- **The grants**: the machine's runtime is granted, for each bucket a module it carries owns, writing
under the bucket's own subjects and reading; for a bucket it only reads, reading. Measured once built,
with the composed grants loaded into a server: a reader's write is refused by the server.
- **The membership** issued to each assignment lists its buckets by the names the module uses, and
whether it may write.
- **The runtime** answers `mesh/state.get`, `put`, `delete`, `keys` and `watch` on the bundle's channel.
A watch hands the current values — none that is deleted — then every change, each naming the watch it
belongs to; it is answered once the current values are delivered. The runtime refuses, with the
reason, a state the module was not issued, a reader's write, a key the bus cannot hold, and a value
carrying a field named like a credential.
- **The SDKs**: `state(name)` in TypeScript, `stdio.State(name)` in Go (tag `go/v0.1.7` and later).
## What the first live use showed
- **A refused request is a timeout, not a refusal.** The bus reloads a machine's grants a moment after
the push that changed them; a bundle that asks in between waits out its deadline. A module that
watches at start therefore watches beside its handshake and asks again until the state answers — the
agent module needed two to seven attempts on its first start on each machine.
- **A late machine reads the whole set.** A server registered for every machine before one machine was
assigned the module reached that machine from the current values at its start.
- **The secrets guard is partial and works for what it covers**: an entry carrying an `Authorization`
header was refused on the live bus. A sealed value is plain text to an inspector, and is not caught.
## How it is checked
The controller's catalogue and broker tests (names, grants, memberships, a bucket asserted in place
against a real server); the runtime's tests over a real bus (current values without deletions, refusals,
the TypeScript SDK through the runtime); `module check` names a read whose owner on the shelf keeps no
such state.
@@ -0,0 +1,57 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/claude-code, mesh-catalog modules/claude-licence-manager]
updated: 2026-10-04
decisions:
- "ADR 0183"
- "ADR 0206"
- "ADR 0209"
---
# The operator's agent and its licences, as they run
**Every machine with an operator account runs the agent module, and one licence manager on the control
node keeps the licences.** Both are Go binaries the machine's runtime launches; neither has a container,
a port or a bus credential of its own. Live since 2026-10-04, on all four machines.
## The agent module on each machine
- **Writes the agent's managed directory**: the tool servers — the mesh MCP server as `mesh`, plus servers
registered through the module — the mesh's settings, and the instruction file. The tool-server list
is exclusive by the vendor's rule: a server not in it does not load on that machine.
- **Keeps registered tool servers in its state**, one key per registration for every machine or for one;
each machine renders what applies to it.
- **Reports what its machine holds** — the account its agent names, the kind, fingerprints and expiries,
never a token — at start and whenever the credentials file changes.
- **Writes what the machine should hold**: on a newer generation of its binding it asks the seat's
`current`, sealed to its own key, and writes the access token only. No machine holds a refresh token.
- **Hands over a login when asked**, sealed to the manager's key, and **adds an API key** from a file on
its machine the same way, removing the file once the manager has it.
## The licence manager on the control node
- **Holds the `anthropic-licence-manager` seat**: `licences`, `bindings`, `bind`, `switch`, `release`,
`refresh`, `usage`, `adopt`, `public-key`, `current`.
- **Learns licences from the reports**: a refresh token it does not hold is adopted by refreshing it,
newest login first, once per account. The machine a login was made on is moved to that login's
account; a machine bound to nothing is bound to the account it reports.
- **Is the only refresher**: every exchange under a lease per licence in its own database, every four
hours and in any case within an hour of expiry; grants are encrypted at rest with a key the vault made.
- **Publishes what each machine should hold** as its `bindings` state, with a generation that grows with
every rotation and switch.
## On the day it went live
One subscription account was adopted from the control node's own login on its first start; the other
three machines, logged in to the same account with older logins, were bound to it without their logins
being exchanged. A forced rotation reached all four machines within seconds. Two faults were found and
fixed while it rolled out: a machine reporting an already-adopted account later was never bound, and a
seat verb named with an underscore was refused by the builder.
## How it is checked
Each module's own tests (the agent's instruction file held byte for byte to the renderer it replaced; the
manager's rules on a store in memory and a stub vendor; its store against a real database); one run of
both binaries under the real runtime with a stub vendor before going live; and live: `licences` lists the
licence with every machine bound, and each machine's `claude_code_status` names it with no login waiting.
+5 -1
View File
@@ -15,11 +15,15 @@ Where the two disagree, the implementation wins and the disagreement is stated.
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves | | [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being | | [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live | | [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for | | [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model | | [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts | | [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says | | [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do | | [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
| [`14-a-modules-state.md`](14-a-modules-state.md) | A module's current state on the bus: what the controller creates, the runtime serves, and the first live use showed |
| [`15-the-agent-and-its-licences.md`](15-the-agent-and-its-licences.md) | The operator's agent on every machine and the licence manager that keeps its licences |
## What these documents are not ## What these documents are not

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