diff --git a/03-DESIGN/01-to-be/06-the-controller.md b/03-DESIGN/01-to-be/06-the-controller.md index d2e59aa4..cbaaa56a 100644 --- a/03-DESIGN/01-to-be/06-the-controller.md +++ b/03-DESIGN/01-to-be/06-the-controller.md @@ -3,8 +3,9 @@ layer: to-be status: in-progress code: - mesh-controller -updated: 2026-10-10 +updated: 2026-10-12 decisions: + - "ADR 0315" - "ADR 0001" - "ADR 0005" - "ADR 0006" @@ -64,11 +65,20 @@ applications** — first-party, shipped with everything else, and running on the anything else does. A task does not need to know a node exists, and *being ours does not make something infrastructure*. `ai` is folded into `config`: a provider licence is an ordinary grant. -**`record` is an open question rather than an eighth entry.** Contexts integrate through it -(ADR 0008), which makes it load-bearing, -and *where it lives* is unresolved, tracked by issue 414 since -research 006 closed — putting it in the foundation risks recreating the circularity the tier design just -removed. Listing it here would settle by naming what has not been settled by arguing. +**`record` is not an eighth entry: it is the bus** (2026-10-12, ADR 0315). Contexts integrate through +it (ADR 0008), and where it lives was left open by research 006, for fear that putting it in the foundation +would recreate the circularity the tier design removed. ADR 0315 writes down what later records built: the +record is the bus's, the EVENTS stream, where each module says the events it declares (ADR 0041), and the +key-value buckets each module declares for its current state, which another module names in `reads` to read +(ADR 0201). Asking another context is a call to its verbs over the bus, request and reply, and is not the +record; a secret, and whatever a context keeps out of its buckets, is asked for. The controller's own records +are read from its record mirrors (ADR 0289) or asked of its verbs, never from its store. The record lives in +the foundation's bus, not in the store, which stays each context's private database reached only with that +context's own credential, so no store holds a record shared between contexts and the circularity does not +return. No module owns the record whole: each event and each bucket belongs to the module that declares it, +which alone writes it. *How it is checked:* where each rule was decided — the runtime's refusal of a bucket +not issued or a write to one only read (ADR 0201), one credential per context's store (ADR 0008), and the +mirrors' events counted live and compared with the store by probe D17 (ADR 0289). **One of the seven is built.** `inventory` owns a database of that name and holds the node records; the rest do not exist. What it takes to run any of them — the language, and what must already be @@ -225,8 +235,8 @@ every public name. - ~~**The contexts themselves.**~~ **Decided** — seven, by ADR 0006. - What remains open is narrower and named there: **where the record lives**, which research 006 - leaves unresolved because the foundation is the one place it must not go. + What remained open, **where the record lives**, is decided by ADR 0315: on the bus, in the EVENTS + stream and the buckets each module declares, never in a store (above). - **How far it may be split.** One deployable today. Splitting a context out costs the single interface a surface depends on (research 011). diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index 7fe64154..05453177 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -2,8 +2,10 @@ layer: to-be status: in-progress code: [mesh-controller internal/catalogue] -updated: 2026-10-08 +updated: 2026-10-12 decisions: + - "ADR 0312" + - "ADR 0309" - "ADR 0262" - "ADR 0225" - "ADR 0112" @@ -151,6 +153,24 @@ ADR 0107). never created, owned or removed by the mesh. The module requires read or read-write access. Where the data is, is an operator value on the assignment. +*A read-write access writes through the path's own group (2026-10-12, ADR 0312, extending ADR 0051; the +build is tracked by issue 537):* when a module declares a `read-write` access, the node-engine reads the group +that owns the path as it applies, and makes the identity the module's code runs as a member of it: a +container's process gains it as a supplementary group, and an account that runs the module's tools is put in it +as ADR 0252 puts an account in a group. The module then writes the path as the path's group may, and as nothing +else. The path itself is never changed: no owner, group or mode is set on it or under it, and a read-write +access whose path's group cannot write is stated unhealthy, saying that the operator decides. A `read` access +grants no group. A path owned by group 0, or by a group that would let the identity act as root, is refused by +name. A group the node-engine granted is taken back when no declared read-write access asks for it any more, +never one the identity held before (ADR 0252 §3). A container is recreated with the group, and a service of +the machine's own service manager that runs the module's tools as an account is restarted once the account +holds it; a unit of the account's own service manager, or a login session, waits for a new login as ADR 0252 +§5 and ADR 0254 say. The tool runner's account so gains the groups of every read-write access of the modules it +serves: the boundary is the account's, not the module's. *How it is checked:* the node-engine's tests ADR 0312 +lists (the group given and the path's owner, group and mode unchanged; a read access granting nothing; an +unwritable group unhealthy and group 0 refused; the group taken back; the container recreated and the system +unit restarted holding the group). + **A port** is what ADR 0038 already decided: the module says which port its software uses, and the node-engine answers with where the machine put it. @@ -212,6 +232,20 @@ gains a setting by shipping its current value as the default, and `settings` lis source: the default, the mesh, or the node. The first module to use it is the notifier, whose font size and width are preferences. *How it is checked:* the table in ADR 0262. +*A setting left unset reads a value the modules share (2026-10-12, ADR 0309, changing ADR 0164 §7 and §8 and +ADR 0262 §3, §7, §8 and §10; the build is tracked by issue 415):* every module keeps declaring each setting it +takes, so any one module can still be set apart, and a setting may also name a mesh-wide value it shares with +other modules, such as `desktop.font-size`; the desktop modules are the first to declare the shared names they +read. When none of a module's own layers sets one of its settings, the setting reads the mesh-wide value of its +shared name, and only when that is unset too the module's own default. Read from the bottom, last winning: the +module's default, the shared value, the module's mesh-wide layer, the node's. `settings` sets and clears a +shared value without naming a module, and still refuses a write without a module that names no declared shared +name. Both the listing of one module and the listing of preferences across all modules name the shared name as +a value's source, beside the default, the mesh and the node. Setting a shared value is held to the strictest +rule among the settings that read it, so a trusted setting (ADR 0277) is never gone round through its shared +name. Whether a shared value is a number, a colour or a string is left to the typed-settings work, issues 346 +and 410. *How it is checked:* the table in ADR 0309. + *The operator's value travels only where it is asked for (2026-09-30, [issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):* a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking diff --git a/03-DESIGN/01-to-be/32-what-a-module-declares.md b/03-DESIGN/01-to-be/32-what-a-module-declares.md index de6cb6a1..691f146e 100644 --- a/03-DESIGN/01-to-be/32-what-a-module-declares.md +++ b/03-DESIGN/01-to-be/32-what-a-module-declares.md @@ -12,8 +12,9 @@ code: - mesh-tools src/main.ts - mesh-catalog modules/mesh-catalog - mesh-tools node-tools/internal/runtime (a module's state, ADR 0201) -updated: 2026-10-07 +updated: 2026-10-12 decisions: + - "ADR 0307" - "ADR 0240" - "ADR 0233" - "ADR 0201" @@ -260,6 +261,17 @@ them on the bundle's channel — refusing, with the reason, a name it was not is bucket it only reads. A watch hands the current values first, then every change: a bundle that starts late, or starts again, has the whole of the state before it has any of the news. +*A key's kept values are read too (2026-10-12, ADR 0307, extending ADR 0201).* A bucket keeps as many past +values per key as its owner declared, and the runtime serves them: `history`, given a state and a key, answers +every value the bucket still keeps for that key, oldest first, each with its revision, whether it was a put or +a delete, and when it was written; an empty answer for a key it holds nothing for. It is a read like `get`: +served to the owner and to every reader, refused with the reason to a module the bucket was not issued to, and +it writes nothing. The Go SDK's `State(name)` carries it as `History(key)`, beside `Get`. Its first user is the +issue tracker's switch to its log (ADR 0300 §6), which carries every value its bucket still keeps; that work is +tracked by issue 502. *How it is checked:* the runtime's tests (several values and a delete answered oldest +first, with revision, operation and time; an empty answer for an unknown key; a reader served, a module the +bucket was not issued to refused, nothing written) and the SDK's tests of `History`. + The owner says how many past values a key keeps and how long a value lives, as a seat says how long its backlog survives (§3); the mesh caps a value's size and a bucket's. **A bucket outlives its module's assignment** — what a module stored is data diff --git a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md index 287efc33..2a5c8705 100644 --- a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md +++ b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-host, mesh-controller, mesh-catalog] -updated: 2026-10-09 +updated: 2026-10-12 decisions: + - "ADR 0310" - "ADR 0268" - "ADR 0252" - "ADR 0203" @@ -88,6 +89,7 @@ WP4 the service manager's module, finished (mesh-catalog) independ WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled WP6 a module puts the account in a group (mesh-host, mesh-controller, mesh-catalog) issue 247 WP7 the control-node withholds execute (mesh-controller, mesh-catalog) ADR 0268 +WP8 a node's files and accounts are read (mesh-sdk, mesh-catalog, mesh-controller) ADR 0310, issue 418 ``` WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository @@ -334,3 +336,33 @@ out, the control-node's value is set to `withhold` and zsh is pushed to it. Then 2. the control-node's `zsh_config` answers `execute` as not served, and says why; 3. `mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node; 4. a call to the control-node's `node-login-shell.execute` is refused, and the other nodes still answer. + +## WP8 — A node's files and accounts are read through `node-reads` + +*`mesh-sdk`, `mesh-catalog`, `mesh-controller`. ADR 0310, extending ADR 0268; tracked by issue 418.* + +ADR 0268 §5 has narrow read tools take the common reads over from `execute`, so that an approval is asked for an +act and not for a read, and ADR 0286 §44 sets the approval mark on `execute` only once those reads have a tool on +every node. Two had none: reading or searching a file, and looking up an account or a group. + +- **A node seat, `node-reads`, on every node**, the control-node included, held by the tool runner; the + controller holds the seat's definition beside the other node seats. +- **Three reads:** a file read bounded in size, a search by fixed string bounded in matches and in depth, and a + lookup of an account or a group. Each verb says what it replaces (ADR 0245): `cat`, `grep`, `getent passwd`, + `getent group` and `id`. The bounds are set by the build and stated in each verb's description. +- **It reads as an account of its own**, which the mesh creates on every node, in no group that holds secrets, + with no login shell and no home of the operator's, so the node's file modes bound what it sees. `/proc` and + `/sys` are read as far as that account may. +- **Secret places are refused by name** before any read: the vault's directories, every module's own-secret + directory and the known credential files, whatever the file modes would allow. +- **One content rule.** The rule the operator channel applies to what it sends moves into `mesh-sdk`, and both + the operator channel and every `node-reads` answer apply it. + +Once the seat answers on every node, the approval mark on `node-login-shell.execute` is set as ADR 0286 §44 says, +under the same issue. + +**Proof.** The rows of ADR 0310's *how it is checked*: the controller's seat listing names a holder of +`node-reads` on every node; the tool runner's tests of the bounds, the account and the refusal by name; one +`mesh-sdk` test of the content rule and one in each of its users; `mesh_search` for `cat`, `grep` and +`getent passwd` finding the verbs. Live, the kernel's RAID status file is read through the seat on the home +server, and the agent's credentials file on the laptop is refused. diff --git a/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md b/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md index a021773a..174fe209 100644 --- a/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md +++ b/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-catalog, mesh-controller, mesh-host] -updated: 2026-10-10 +updated: 2026-10-12 decisions: + - "ADR 0303" - "ADR 0215" - "ADR 0173" - "ADR 0040" @@ -120,6 +121,20 @@ not write into its include directory. Each contribution is a dependency on the s so assigning one of them without a window manager is refused. The first versions, which still write the include files themselves, move to contributions once the controller derives the dependency. +**A package may be declared by several modules** (2026-10-11, ADR 0303, changing ADR 0210 §1, §10 and §11 +for packages). Two modules on one node that each need a program may each declare its package, and it is +present while any of them declares it. Declaring a package asserts only that it is present: the node-engine +installs one the machine lacks, never upgrades it, and never removes it because a declaration went away, so a +second declaration changes nothing on the machine and taking one module off leaves the package to the others. +A module that needs a program another seat holder installs may declare that package itself, or still depend on +that seat. One module declaring a package present while another declares it absent (ADR 0180) on one node is +refused at assignment and at composition, naming both. A tool's configuration files and the contributions to +its seat keep their one owner, as above: only the package itself may be declared by several modules. *How it +is checked:* the controller's tests (two declarations of one package are assigned; present against absent is +refused, naming both modules) and the node-engine's tests ADR 0303 requires (a package declared twice is +installed once, one no module declares any more is not removed, an installed one is reported unchanged and not +upgraded). + **Added 2026-10-04.** `triggerhappy` holds `node-hotkeys` on both workstations (ADR 0212). The laptop model's vendor keys become its contribution. The window-manager fragments of the launcher, the clipboard, the wallpaper, the bar and the laptop model become `config` contributions to diff --git a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md index 8fb7183f..e762ae39 100644 --- a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md +++ b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab] -updated: 2026-10-11 +updated: 2026-10-12 decisions: + - "ADR 0314" - "ADR 0293" - "ADR 0289" - "ADR 0285" @@ -531,6 +532,22 @@ the controller's replay R301 and its tests of the rule). > its cause, so the exception is read from the verbs. Nothing decided changes: ADR 0230's acts stay > exempt, and pushes by hand — what roll-out by default (ADR 0236) exists to end — still count. +**A condition no healer can fix becomes an issue** (2026-10-12, ADR 0314, extending ADR 0285; the build is +tracked by issue 409). A condition no healer can fix is one whose kind no healer answers, one a healer passes +over because it does not apply, or one handed to the operator after a healer's budget was spent. When such a +condition is raised, passed over or handed on, and no issue links its key (ADR 0285), the issue tracker opens +one itself, from the controller's condition events, with the condition's summary, its evidence and its key, and +links the key at once; the controller sets no link. Every message about the condition then names the issue +through its tracking line. Agent sessions take such an issue as any other: a claiming move that names the +worker, a branch, review and the required checks; no standing agent acts on a condition by itself. What an +agent does about it is written in the issue's notes, and every act it makes by hand on the mesh is also written +to the hand-act log, which S15 reads. ADR 0275 §9's week stands: the tracker opens no issue for a condition +before 2026-10-17 20:17 UTC, and until then such a condition is linked by whoever opens an issue for it. *How +it is checked:* the tracker's tests feeding condition events (one issue opened and linked for each of the three +cases, none for a condition a healer acts on or one already linked, none before the week ends); live, the +messenger's preview of such a condition says "Tracked as issue N", and the issue's notes and the hand-act log +hold what the agent did. + ## 8. Staged core upgrades and rollback (rule 8) **Health, per core component**, as probes in the registry (§4): @@ -881,4 +898,3 @@ in [to-be 47](47-delivery-from-commit-to-delivered.md), and their progress is tr - Routing by presence, quiet hours, answering back through a channel, and an external dead-man service — decided by ADR 0234, designed in [to-be 46](46-the-conversation-with-the-operator.md). -- A condition that needs judgement handed to an agent as work — tracked by issue 409, since research 017 closed. diff --git a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md index 05b9f72b..da34cd72 100644 --- a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md +++ b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog] -updated: 2026-10-10 +updated: 2026-10-12 decisions: + - "ADR 0308" - "ADR 0286" - "ADR 0277" - "ADR 0259" @@ -289,6 +290,40 @@ Presence facts are events on the bus. The router keeps them as **current state o machine and per intake kind, overwritten — never as a history, and never in a message's words. A consumer that wants a timeline of the operator's day is a decision of its own. +### While a delivery walks + +*2026-10-12, ADR 0308; the build is tracked by issue 532.* + +A delivery restarts what it moves, and those restarts raise conditions that clear once it is done; a router +that restarted used to say again what it had been told to keep quiet (issue 531). So the router holds what a +walk touches and tells the walk's result once. + +- **A walk names what it touches.** The controller's walk events say, when a walk starts and again when it + ends with its outcome (delivered, failed or stopped), the modules it moves and the machines it sends them to. + What it touches is those modules on those machines, and every module a moved tool runner or node-engine + carries there. +- **The router holds what an open walk touches.** A message whose subject is a touched module or machine is + held instead of sent, kept with when it was held, and `history` says it was held. A message several open + walks touch is held until the last of them ends or its bound passes. +- **Some messages are never held:** an ask (§6), a secret given or replaced, a message about anything no open + walk touches, and a walk's own failure. +- **A walk's end is told once:** one message with its outcome, the modules and machines it reached and how + long it took, then each held message whose condition is still open, one line each. One whose condition + cleared during the walk is only counted. A walk delivered with nothing held and nothing open is one line, or + nothing on a channel set to say only what needs the operator. +- **A hold has a bound:** twice the largest delivery budget in force among the walk's deliveries + ([to-be 47](47-delivery-from-commit-to-delivered.md)): 20 minutes for a core module, 10 for leaf modules at + a five-minute budget and 6 at a three-minute one; 15 minutes, not doubled, for a walk none of whose + deliveries has a budget. Past it, every held message still open is sent with a line saying the walk still + runs and since when, and the cleared ones are counted. A walk that hangs never silences the mesh. +- **A restart says nothing it has said:** a router that restarts says again only what is open and was never + answered, never a silenced message or one withheld for its content. + +*How it is checked:* the controller's tests of the walk events, and the router's tests of holding, never +holding, the end message, the bound and the restart, as ADR 0308 lists them; live, the next delivery of the +tool runner gives the operator one message at its end, and the channel's `history` shows what was held and +released. + ### The content rule The router's, applied before anything reaches any holder; §9 says what it allows. A `private` holder diff --git a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md index 3625ebea..378763d9 100644 --- a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md +++ b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-controller, mesh-catalog modules/mesh-delivery] -updated: 2026-10-11 +updated: 2026-10-12 decisions: + - "ADR 0306" - "ADR 0299" - "ADR 0298" - "ADR 0282" @@ -232,6 +233,32 @@ shown and held to no budget. Measured on 51 walks on 2026-10-09 and 10, a leaf w - **Every phase is kept**: the window, the wait, the build, the send, each reading, the rest's first healthy report; the plan shows an expected landing time from the last seven days' medians. +*Walks beside each other (2026-10-12, ADR 0306, extending ADR 0304, which narrowed the bullets above to what +a change shares).* Building the walks at once found what those bullets left open, and ADR 0306 decides it: + +- **A walk's builds are its own, never another's backlog.** A send composes a module whose build another open + walk owns at the build its node was last sent, and neither carries nor judges it: that build is moved and + judged only by its own walk, so a failed walk puts back only its own builds. What waits on a node with no open + walk to move it is still backlog, carried and judged by the next gated send there (this changes ADR 0236 §17 + for builds an open walk owns). +- **A delivery group still assembles.** A merge whose delivery group has a member not yet merged waits for the + merge window whether or not a batch is assembling, so a group's members walk in one batch, in their order. +- **Related means everything the walks would move.** A module in common is any module a walk plans to build or + send, the dependents of what its merges moved included; a change whose modules are not known yet is related to + every change. Two related walks are neither cut beside each other nor judged beside each other on one node. +- **A starved judgement is said.** A leaf judgement set aside by core builds three times in a row raises a + warning naming the walk and the core walks that set it aside. +- **A node's own fault fails every judgement on it.** A node found unwell as a whole (a failed apply, an + unhealthy network or engine) fails every judgement open on it, leaf and core, each walk putting back only its + own builds. +- **A rollback beside another walk's judgement** behaves as it does today; whether it should wait is tracked by + issue 528. + +*How it is checked:* the controller's tests ADR 0306 lists, one per clause: an unsent build of walk A left out +of walk B's send and not taken up as backlog while A is open; a group's second member joining its first's batch; +overlapping dependents keeping two walks apart; the warning on the third set-aside; a failed apply failing every +open judgement on its node. + **Who builds what.** | Part | Owner | What it adds | diff --git a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md index ca2e748f..06dad5ed 100644 --- a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +++ b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md @@ -6,8 +6,9 @@ code: - mesh-catalog modules/artifact-store-tools (the store's tools), modules/distribution (its unbuilt code removed) - mesh-catalog modules/gitea (package retention) - mesh-catalog modules/docker (image pruning) -updated: 2026-10-08 +updated: 2026-10-12 decisions: + - "ADR 0311" - "ADR 0251" - "ADR 0189" - "ADR 0257" @@ -110,7 +111,8 @@ ADR 0257: failed one, and the controller keeps a row per copy in `artifact_mirrored`. The copies the existing build records already named were recorded by the migration that made the table. - **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never - keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. + keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. *(A copy of a pinned + third-party image is the exception, below: ADR 0311 §3.)* - **Held whole.** A copy counts as held only when its index and every platform the index names are held; otherwise it is copied again. A copy a build copies again is no longer recorded as let go of. - **An index goes with its platforms only in a confirmed collect.** Before its first delete, it reads @@ -122,6 +124,35 @@ ADR 0257: `unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a person names, and the sweep then decides about them. +### Pinned third-party images + +*2026-10-12, ADR 0311, extending ADR 0257; the build is tracked by issue 421.* A third-party image a module +pins was held only upstream, and an image withdrawn there was found only when a node that lacked it tried to +deploy (issue 113). So the mesh holds a copy of what its modules pin, and checks both sources: + +- **Every pinned third-party image is copied** into the artifact store when it is first used, into the + repository of its upstream image (`upstream//`, as above). +- **Its record names the pin.** The copy's row names, as its reason, the module whose definition pinned the + digest (changing ADR 0257 §6, which named only a build or a person's reason). +- **Kept while a module pins it.** A copy is kept, with the reason `pinned`, while any module's current + definition pins its digest; neither the sweep after a build nor the nightly clean-up lets it go. For these + copies a definition does keep a copy, and one is eligible only when no kept build stood on it and no current + definition pins it (changing ADR 0257 §8, §9 and §20). `artifact_store_references` shows them with the reason + `pinned`. +- **A node pulls from the mesh first**, from the artifact store's copy, and from the upstream only second. +- **A scheduled check pulls every pinned image as an empty node would**, once from the copy and once from the + upstream. An upstream that no longer serves while the copy does raises a warning condition naming the module + and the image; neither serving raises an urgent one; each clears once the image is served again. An upstream + that serves while the copy does not is not decided yet (issue 421); a node then still pulls from the upstream. +- **A module may declare its upstream**, the object store's module first, with its maintained fork, and a view + shows each declared upstream's last release and open advisories, so ADR 0288's triggers of no release for six + months and of an advisory unpatched for 30 days can be read there. + +*How it is checked:* the table in ADR 0311 — the controller's copy and its row, the collection test of keeping +by pin, the node-engine's pull from the store with the upstream unreachable, the check's two conditions, the +manifest test of a declared upstream, the view against a recorded answer, and live, the object store's pinned +image held in the store. + ### The controller's verbs for the store - **`artifacts`** reads only. It answers every reference the mesh recorded making, with its state and,