Compare commits

...
Author SHA1 Message Date
jschoubben 831f34e59a mesh-build-go: the real-go integration tests expect the tests to run after a failed gofmt or vet (ADR 0591)
mesh/check checking 831f34e59ac7 on c52afdf88b84 | novox/mesh-catalog#596 in group fix/check-every-failure
mesh/delivery-group group fix/check-every-failure checking: 2 of 2 member(s) ready
mesh/delivery closed: the pull request closed unmerged
The group's check named TestUnitLevelRunsTheRealGoOnTheFixtureRepositories and
TestAnUnformattedFileFailsTheRealUnitLevel: both asserted the old stop after the lint.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 17:00:30 +01:00
jschoubben 86be014907 mesh-build-go, mesh-build-node: Pin mesh-sdk at 43788bb, the group's head: a check ask runs every level after a failure
mesh/check failed (2 tests): integration: meshbuildgo/cmd/mes… | 86be0149073c on c52afdf88b84 | judged by the candidate's mesh-build-go at a42cbc56ec2e
mesh/delivery-group group fix/check-every-failure rejected: composed together: the checker's verdict at the members' heads (468d3ec8bcb2): integration: meshbui…
mesh/delivery closed: a newer head of the same pull request
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 16:33:12 +01:00
jschoubben 06358f94bd mesh-build-go: a unit level whose gofmt or vet fails still runs its tests, so one check names every failure
The operator, 2026-11-02: a failed check showed only its first fault, and a group needed a run per fault.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 16:32:28 +01:00
mesh-tools c52afdf88b Merge pull request 'The vault makes the first credential of every new consumer, as the controller did (issue 1126)' (#595) from fix/1126-provision-credentials into main 2026-11-02 15:31:37 +00:00
jschoubben 019614aedc mesh-vault: make a new consumer's first credential whether or not it is handed
mesh/delivery merged: source.main moved to novox/mesh-catalog@c52afdf88b84
mesh/delivery-group group fix/1126-provision-credentials merged: every member is merged
mesh/check pass 019614aedc14 on fe7c5ccb64bf | unit 6028 run, 142 kept, 16 skipped | integration 6091 run, 174 kept, 25 skipped | over bound: unit, in…
With the controller gone nothing made the pair row of a new consumer: the vault minted only for a provider
handed to the provisioner (ADR 0349 clause 17's premise, that a holder filled the old way gets the controller's
value). A node composing its own now asks the vault for its credentials (own, ADR 0547 §2, ADR 0550 §5), so the
vault makes the first value of a shared or per-login provision for every holder, create-only (issue 1126; ADR
0570 §6). A remake or a rotation still waits while a holder is filled the old way. A credential asked of the
vault in mesh-provisioner's provisions starts a pass at once, one pass at a time, and vault_status answers each
provider and provision's last outcome as `credentials`.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 15:37:23 +01:00
mesh-tools fe7c5ccb64 Merge pull request 'Modules without the controller: vault judges each sensitive caller, nox records its hand acts, provisioner binds ahead of the want (issue 1126 group B)' (#594) from feat/1126-removal into main 2026-11-02 13:40:53 +00:00
jschoubben fa6dc3a48b Retake a pin whose forge listing failed, naming the forge's cause
mesh/delivery merged: source.main moved to novox/mesh-catalog@fe7c5ccb64bf
mesh/delivery-group group feat/1126-removal merged: every member is merged
mesh/check checking fa6dc3a48b0c on 32c4c52261c4 | novox/mesh-catalog#594 in group feat/1126-removal
When the forge's listing of pull request heads fails, the fetch now says it
as a download failure that may be retaken, so a forge outage is asked again
and not held as a refusal (issue 1748, issue 1126 review 3).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:52:10 +01:00
jschoubben 7c5df1d7aa Decide a pin on a pull request head from the forge, not from go's words
The group's check has no name lookup, so go failed on the network before it
could say it knew no such revision, and the fetch, reading go's words, took
the asked head for a download failure. Whether a commit is a pull request's
head is now the forge's fact, asked whenever go cannot fetch it; go's words
only label a commit on no head (issue 1748, issue 1126 review 2).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:49:38 +01:00
jschoubben 7f8701637f Pin mesh-sdk to the group head of issue 1126
mesh/delivery-group group feat/1126-removal rejected: composed together: the checker's verdict at the members' heads (a523066f1791): integration: meshbuildgo/c…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go… | 7f8701637f9d on 32c4c52261c4 | judged by the candidate's mesh-build-go at b82f7f676986
Every member builds against mesh-sdk abf5eac, the head of feat/1126-removal, so the group is checked as one contract.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:57 +01:00
jschoubben 067f2f4d61 Record no hand act for a cleanup that deletes nothing
nox cleanup delete without --confirm deletes nothing; the vault refuses it
for its missing confirm alone and judges only a confirmed delete, and nox
records the warranting act only for that one (issue 1126 review 2).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 0ef81d85b3 Count nox's warrant acts for a vault verb as the operator's decisions
nox records a hand act before each sensitive vault verb, with the verb's
address as its verb and cause operator; two in a fortnight raised a false
healer-wanted. mesh-healers reads those four verbs as decisions by design
(issue 1126 review 2).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 04379702e5 Pin mesh-sdk to the group head of issue 1126
Every member builds against mesh-sdk f569924, the head of feat/1126-removal, so the group is checked as one contract.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 9a3ead8fcd Carry the plan entry only on a key no plan names
A reader on an older mesh-sdk refuses a provision field it does not know,
so a key a plan names carries no entry: such readers skip only a key ahead
of its consumer's want, which they could never bind (issue 1126 review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben fda81c129e Key each provision with the plan entry it is composed from
mesh-provisioner keys provisions from a consumer's wanted and newest builds
but writes the wanted build's plan alone, so a requirement the newest build
adds was never bound. Each key now carries its plan entry, and a key ahead of
its want says in its why that no plan names it yet and its provider binds it
from the entry (issue 1126 review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben ecc7621274 Privilege no caller named controller in the vault; match a hand act's verb exactly
The controller is not placed (ADR 0590), yet the vault still answered a
caller named controller alone (accept), took its relay of who asked
(secret-ask, rotate's by) and sealed it the core's credentials. Those
branches and accept go with their tests; a node's account alone is never
minted or rotated here. The sensitive-verb judge now needs the act's verb to
be the vault's own, mesh-vault.<verb>, not any verb ending so (issue 1126
review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 3ed3266dde End healers H3 and H4 and the bus-health probe's consumer half with the controller
Nobody raises a lost or a far-behind consumer since the self-check's bus-consumer probe ended (issue 1126): the grants
part makes a missing account consumer itself and says one far behind as mesh-grants.consumer-behind, and H4's repair
was the controller's. The mesh defines no consumer of its own, so H-bus's consumer half judged nothing.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 9e9877e6ec Let one hand act warrant a retirement of several consumers
mesh-hand-acts records one what per act, so an approve of two consumers
could never be warranted. The judge reads each word of the act's what as a
name, and nox records one act listing the consumers (issue 1126 review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 0f0ec66e9a nox records the hand act a sensitive vault verb is warranted by
Each sensitive vault verb judges its own caller, and nox reaches the vault
as its machine's runtime, never as a person, so it could use none of them.
Before rotate, retire approve, cleanup delete and secret export nox records
the act through mesh-hand-acts (the vault verb as its verb, what it acts on
as its what) and names it as act; a refused recording calls the vault not at
all (ADR 0590 §8, issue 1126 review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 344fc6f6bf End the self-check's bus-consumer probe, whose consumers were the controller's
With the controller ended mesh-sdk defines no mesh consumer, so the probe judged none and always passed (issue 1126).
The account consumers are the grants part's, which now says their differences and delays as conditions of its own;
the probe's three kinds are carried as ended in mesh-conditions' table.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 70396caeab Pin mesh-sdk to the group head of issue 1126
Every member builds against mesh-sdk 729a86b, the head of feat/1126-removal, so the group is checked as one contract.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 4aa24ba013 Drop mesh-conditions' use of operator-channel, since nothing in it asks the operator
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 067117b8b8 Point the nats README's bus upgrade at the assigner's put and mesh-wants, since the controller's bus verb ended
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben e7b3fd470b Prove the bus tools' removals on the grant mesh-sdk composes
The removal test took a grant written by hand; it now composes it with
BusRemovalGrants for controller-leftovers and the node-engine's acts, so
it proves the mesh's own grant is enough and no wider (issue 1126 B5).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben f8e4c1b0ae Stop serving the vault's move-asks, which only the controller called
move-asks copied the controller's record of asks into the vault's once;
the controller is not placed (ADR 0590) and nothing else calls it. move-in
stays: the node-engine calls it. The drift test names the node's own
composition now that the controller composes for none (issue 1126).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben a73b02ea30 Drop the keeper's and config's take-over verbs with the controller's sources
mesh-sdk drops compare, take-over and hand-back from the keeper's and
config's seats (ADR 0590): there is no controller to compare with or hand
back to. mesh-conditions loses those verbs, its take-over key and asks;
mesh-config reads its own sources always and pull-declares is gone; and
mesh-hand-acts no longer names the controller's log (issue 1126).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben f83e6e4713 Make each sensitive vault verb judge its own caller
The controller judged the operator's account for approving or deleting a
retirement, rotating a secret and exporting sealed copies; it is not
placed, and these verbs are reached through the mesh MCP server on each
machine. Each now answers the operator's account (person.<name>), or a
caller naming its own recorded hand act for that verb and subject, and
nothing else (ADR 0590 §8, issue 1126).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben f723d4372f Let claude-code read its tools and machines from the bus, not the controller
The controller is not placed (ADR 0590), so its tools, modules, nodes and
node verbs answer nothing. claude-code now reads the seats' verbs from the
seats' contract, the placed modules' replaces from the indexer's manifests,
and the machines and where modules run from mesh-placement's placement; the
rendered instructions name the seats that own each job (issue 1126 B13).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben b44e600e87 Drop artifact-store-tools' controller client; the provisioner client reads placement
The controller is not placed and its assignments mirror is frozen (ADR
0590). artifact-store-tools already answers through the seat's holder, so
its controller client goes. mesh-provisioner-client reads the seat
mesh-placement's placement, which its account is granted whole: a module
placed here stays while the seat holds no plan of it, filled from its last
plan, and leaves only when neither names it (issue 1126 B12).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben ab610ffed4 Read a go download failure as a pin on a pull request head only when go does not know the revision, so a network failure is retaken and says its own cause
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben c4a6452f21 Give mesh-mail-reader's two acceptance tests a due date a week from now, so they do not expire
Both stand-in judges answered the fixed due date 2026-11-01; since 2026-11-02 00:00 UTC that date is past,
signalEntries drops the mail, no tell is sent, and both tests time out on every check that rebuilds the module
(met in the enrolment group's check of issue 774, 2026-11-02 02:50).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben e009088150 Point messenger, mesh-watcher, restic and mesh-issues away from the controller
The controller is not placed (ADR 0590): messenger no longer treats it as
a condition owner or a walk's subject, mesh-watcher reads only where the bus
runs, restic tells the operator to compose the machine or set config through
the seats that do it now, and mesh-issues' examples name a placed repository.
mesh-issues keeps mesh-controller among its owners: issues located there,
issue 1126 among them, and route-proxy until it moves, still name it
(issue 1126 B11).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 40c4fabff6 Let the bus tools remove the controller's leftovers by one word
The controller ended by its unassignment, so its consumers, streams and
buckets may be removed as recorded hand acts (ADR 0590 §6). The nats tools'
account declares controller-leftovers in place of the lease write's two
words, and the removal test proves a consumer of EVENTS and a bucket's
stream go while EVENTS and its other consumers stay (issue 1126 B5).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
jschoubben 0bfc72d230 Make the controller checks strict: nothing names the placed-nowhere controller
controllerdeps refuses any manifest naming the controller's seat, states or
buckets, the lease and the entries once kept to its write among them, and
any module file naming one of its buckets (ADR 0590 §5). The words check
refuses a controller address in any text a module shows. The handacts check
and noplacement's list of assignments readers are folded into it (issue 1126 B10).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00
mesh-tools 32c4c52261 Merge pull request 'nox works without the controller: each command calls the seat or tool that owns its job (issue 1126)' (#593) from fix/1126-nox into main 2026-11-02 10:56:31 +00:00
mesh-tools d2de18fd6b Merge pull request 'Without the controller: the vault retires and rotates, the healers act, the artifact store is swept, config and conditions stand alone (issue 1126)' (#592) from fix/1126-gates-catalog into main 2026-11-02 10:36:34 +00:00
jschoubben 63de1f7e0d nox cleanup delete confirms with the holder's name, as the vault asks
mesh/delivery merged: source.main moved to novox/mesh-catalog@32c4c52261c4
mesh/check checking 63de1f7e0dd7 on 32c4c52261c4
The vault deletes a retired holder only when confirm repeats its name, so
sending confirm "true" could never delete anything. The stand-in console now
also says which machine answered as a block of its own, as node-tools does.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:47:56 +01:00
jschoubben 9523050a98 nox nodes reads placement with the console's answered-by line after it
The console appends which machine answered after the JSON, so the nodes
table decoded nothing and printed the raw answer; read the first value
and keep the trailer.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:44:43 +01:00
jschoubben 326897d877 mesh-config and mesh-conditions without the controller's lease
mesh/delivery merged: source.main moved to novox/mesh-catalog@d2de18fd6bdd
mesh/check checking 326897d8772f on d2de18fd6bdd
The controller ended by its unassignment, so its lease is never handed and
both stayed in their controller-era branch (issue 1126, A5). mesh-config no
longer trusts the controller's relay: a trusted value needs the operator's
Approve. The condition keeper carries nothing from the controller, clears
once at start what it carried, and refuses take-over as the one source.
Both drop their manifest read of mesh-controller.lease.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:43:52 +01:00
jschoubben a3e9f2cc0f mesh-cli requires the console's endpoint and writes its address
nox now reaches the mesh through this machine's console, so the module
requires mcp-endpoint and writes the bound address to /etc/mesh-cli/console
rather than trusting a fixed port. No invokes: mesh-cli has no bus account,
and one nothing dials would only be minted.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:42:03 +01:00
jschoubben 5cdf8295f3 nox calls the seats that own each job, not the unassigned controller
The controller nox forwarded every line to is unassigned (issue 1126), so nox
answered nothing but enrol open. Each line now calls the seat verb or module
tool that owns its job through this machine's console (mesh_call); a line no
seat owns says so and names what to use, never failing silently.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:41:32 +01:00
jschoubben 0e6c358623 mesh-healers act and artifact-store-tools sweeps without the controller's lease
The controller ended by its unassignment, so its lease key is absent for good
and the healers stayed in shadow while nobody swept the artifact store (issue
1126, A4). Drop both gates and each manifest's read of mesh-controller.lease;
the would bucket stays readable until its keys expire.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:38:58 +01:00
jschoubben fe7d4504bf mesh-vault: retire and rotate without the controller's lease
The controller ended by its unassignment and its lease key expired, so the
gate on a handed lease kept retirement, rotation and node-login rotation off
for good (issue 1126, A3). Drop the gate, the lease watch and the manifest's
read of mesh-controller.lease; vault_status no longer reports a lease.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:35:59 +01:00
mesh-tools db104bdb08 Merge pull request 'The vault mints a joining node's token, account and certificate, and rotates a node's login over two; nox enrol open (issue 774)' (#591) from feat/l3-enrol into main 2026-11-02 07:56:27 +00:00
jschoubben c84dfb5e0b Pin mesh-sdk at dff0ce2, the enrolment group's rebased head, so every member builds the same contract (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@db104bdb0838
mesh/delivery-group group feat/l3-enrol merged: every member is merged
mesh/check failed (2 tests): integration: mesh-mail-reader/cmd/mesh-mail-reader.TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt: timed out w…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:44:05 +01:00
jschoubben 67e4d4b062 Name the vault's enrolment wire test by its acceptance criteria alone, not by a review round (issue 774)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:42:29 +01:00
jschoubben f41a5a8df1 Vault: keep a rotation's hand acts in mesh-sdk's NodeLogin.Acts, and test a tunnel without its key or range
mesh-sdk now carries the hand acts that warranted a node's rotations as NodeLogin.Acts (ADR 0583 §2), so the vault's
own `acts` field beside the codec is gone: a used act is refused from the key's Acts, and a key written without them
keeps the ones recorded. The seat's tunnel takes all five fields; the wire table refuses an issue missing the machine's
tunnel key or the range as well. Built against mesh-sdk 4224665; mesh-vault stays at 134 unit tests.

(issue 774, 774/S43, 774/S46, review round 1b)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:42:19 +01:00
jschoubben 09824b4839 Vault: answer the enrolment verbs in mesh-sdk's wire shapes, certify the serving key presented, record each rotation's hand act
Review round 1 found that redeem could never succeed: the vault's answers were not the shapes mesh-enrolment decodes.
- enrolment-token reads `sealed` (not `show`), so asking for the sealed token no longer mints a new one; it answers
  delivery.EnrolmentTokenAnswer: at issue the bus address, its certificate fingerprint and the vault's signing key.
- Issue takes the machine's first tunnel (`tunnel`), keeps it beside the token and refuses a token whose Missing() is
  not empty, so the sealed token is one a machine can use (ADR 0587 §3, insight 2026-11-02).
- node-account answers delivery.NodeAccountAnswer: the login, and the password sealed anonymously to the presented
  raw X25519 key in base64, as the node-engine's SealingKey.Open reads it.
- A joining node's certificate is signed for the serving key presented at the redeem, only when the four presented keys
  have the digest node-account recorded; the name comes from placement (ADR 0587 §6).
- rotate-node refuses a hand act that is not rotate-node of this node, keeps the act id in the node's node-logins key
  (`acts`, beside mesh-sdk's fields) and refuses an id used before (ADR 0583 §2).

The enrolment test now calls the real tool handlers and decodes every answer into mesh-sdk's types, failing on an
empty field; it replaces the earlier enrolment test, so mesh-vault stays at 134 unit tests.

(issue 774, 774/S43, 774/S44, 774/S45, 774/S46, review round 1)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:42:19 +01:00
jschoubben 272eff8b39 Vault: take the node-logins key and the enrolment token from mesh-sdk's contract
The vault wrote its own copies of the NodeLogin key and the token; the node-engine and the grants part read them through
mesh-sdk's codec (delivery.EncodeNodeLogin refuses a key without its version, a login that is not the node's, a stage
without a rotation). It now writes and reads through that codec and builds the token with delivery.Token, so the shapes
are one (ADR 0583 §1 insight, ADR 0587 §3). Built against the group's mesh-sdk with a go.work outside the worktree; go.mod
still pins the earlier mesh-sdk, for the lead to re-pin.

(issue 774, 774/S43, 774/S46, 774/S49)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:42:19 +01:00
jschoubben 9249ca7b57 mesh-cli: nox enrol open opens a sealed enrolment token locally with the operator's private key
The seat mesh-enrolment's tokens answers the token sealed to the operator key; the operator opens it at their own
terminal. The subcommand reads the sealed token on standard input and the private key from a file, prints the token text
a joining machine takes, asks nothing of the node-engine's socket, and says the token is not sealed to a wrong key
(ADR 0587 §6, §8). New: one table-style test function; mesh-cli had 6, so none was merged or deleted.

(issue 774, 774/S51)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:42:19 +01:00
jschoubben 049bd176c3 Vault: enrolment token and node account, signing for enrolment, a node's two logins and their rotation, own hand-out of a login
The vault serves the verbs enrolment-token, node-account, rotate-node, node-account-confirm and abandon, keeps the state
node-logins, and says rotation-begun, node-account-ready and rotation-done (ADR 0587 §3, §5, §8; ADR 0583 §1-§7 and
insights; ADR 0561 §4, §5). Only mesh-enrolment asks the first two and sign for an enrolment; a rotation needs a hand act
read back from mesh-hand-acts by its id, runs one at a time with the bus node last, and is ready only when the loaded list
holds the new login and done when it lacks the old one. A node's login is handed to that node's engine alone, sealed to
the key it stated. The key of a node that leaves the placement is deleted. Accounts kept for an enrolment or a node are
marked and never retired as unwanted.

Subtasks 774/S43 to S50 in one commit: they share enrolment.go, noderotation.go, tools.go and the manifest, and could not
be cut apart without a commit that does not build. New tests: 3 unit functions (table-style flows) and the one
integration test of S50; mesh-vault stays at 134 unit and 2 integration tests, within the module budget of 500 and 50, so
none was merged or deleted. The integration test's player gained a mesh/ask answer.

(issue 774, 774/S43, 774/S44, 774/S45, 774/S46, 774/S47, 774/S48, 774/S49, 774/S50)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:42:19 +01:00
mesh-tools 98e08c0dd0 Merge pull request 'Modules stop depending on the controller before the lease write (issue 774)' (#590) from feat/l3-removal into main 2026-11-02 00:35:23 +00:00
jschoubben 2c5e9c9a98 Pin mesh-sdk at c21d8fd, the removal group's head, so every member builds the same contract (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-catalog@98e08c0dd0af
mesh/delivery-group group feat/l3-removal merged: every member is merged
mesh/check failed: mesh-build-go: the check of novox/mesh-catalog#590: the ask mvyhumdm860c7f's step on shanks did not end within 30m0s of its start |…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:53:42 +01:00
jschoubben e4382d016e mesh-conditions answers hand-back as gone and writes nothing: nothing reads the take-over key any more (ADR 0564 clause 9)
After the group the messenger and the watcher hear the condition keeper alone; a hand-back written to the take-over key
answered follow: controller and changed nothing. It is now answered as gone, naming its record, as placement's adopt and
hand-back are; the verb stays on the seat so a caller is told rather than not found.
Tests: none added; the hand-back lines of TestCompareCountsOnlyWhatBothSidesRaise become a row per lease state, each
answered as gone with the take-over key unwritten.

(issue 774, review g1 round 1)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:51:55 +01:00
jschoubben 2877b87f29 A catalogue test refuses a manifest that still depends on the controller at the lease write (ADR 0571 §13)
checks/controllerdeps decodes every module.json and judges the set with mesh-sdk's ManifestRefusals at the phase of the
lease write's own delivery, so no module reads, invokes or consumes the controller beyond the lease and the entries
kept to the write. At origin/main it refuses 11 declarations (the ones S7-S12 drop); at this head it passes. Two
tests (the real tree, and four rows on a tree of its own), in a new check of its own; go.mod pins the previous
mesh-sdk until the group's head is pinned, and go.sum needs the group's entries then.

(issue 774, 774/S13)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:21:13 +01:00
jschoubben a8c1c275dd messenger hears and asks the condition keeper alone, never the controller (ADR 0564 clause 9)
The three mesh-controller condition-event consumes, the invoke of mesh-controller.conditions, the follow switch
(takeover.go) and its read of mesh-conditions.takeover are gone: the messenger consumes mesh-conditions' events and
reads its open conditions from seat mesh-conditions.conditions at its start. An ask of the keeper is shown on a
condition's message like the controller's was. The manifest test now refuses any controller entry; the event test
refuses the controller's events; the words fixture's action verb names the keeper's. No test added.

(issue 774, 774/S12)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:19:58 +01:00
jschoubben 204a02240c mesh-watcher stops consuming the controller's doctor-heartbeat (ADR 0564 clause 9)
The watcher hears the self-check's `checked` alone: the consume of mesh-controller.doctor-heartbeat, the subscription
to the controller's seat, the follow switch (takeover.go) and its read of mesh-conditions.takeover are gone. The
manifest test now lists the one consume and the heartbeat-key test refuses the controller's event. No test added.

(issue 774, 774/S11)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:18:29 +01:00
jschoubben 432de55186 mesh-hand-acts and mesh-healers stop reading the controller's hand-act log (ADR 0571 §13)
mesh-hand-acts lists acts from its own log alone and no longer copies: its copy tools answer that the copy is over,
with the marker of the copy that was made (the live top-up runs before this delivery). mesh-healers drops the watch of
mesh-controller_hand-acts and counts the seat's acts: it asks the seat's `hand-acts` once at its start for the
fortnight (new invoke) and hears act-recorded after, an act listed and heard counting once, so a restart does not
forget the fortnight. Tests: the two-log listing and the copy table became one listing and one copy-is-over test; the
healers' count test lost its controller-bucket helper and gained the seat listing; the integration test now proves the
old bucket is not copied.

(issue 774, 774/S10)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:17:52 +01:00
jschoubben 2f52f91b5c mesh-config stops reading the controller's predefined list and declared-config mirror (ADR 0565 §4)
Declarations come from the modules' own manifests and the list of predefined keys from the indexer's, whatever the
switch says; compare keeps only the pull request rows; hand-back is answered as gone; a switch that still names the
controller is read as the indexer and said gone. The read-only mirrors, mirrorDeclarations/mirrorList and the value
and list comparisons are deleted with their test cases (two table tests lose their mirror rows, one block of the
newest-list test goes; two table rows added).

(issue 774, 774/S9)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:15:43 +01:00
jschoubben c1a32b4385 The condition keeper stops watching the controller's conditions (ADR 0564 clause 9)
The keeper watches the controller's lease alone; the watch of mesh-controller.conditions, its constant and its manifest
read are gone, so no condition is carried from the controller's bucket any more. The manifest test now refuses that read.
The carry code in the keeper (Controller, the compare tool) stays, unfed, until the controller is removed. No test
added.

(issue 774, 774/S8)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:13:19 +01:00
jschoubben 4497f26257 pacman reads a node's declared packages from the node-engine alone (ADR 0565 clause 6)
The controller's declared-packages mirror ended with its lease: the view no longer falls back to it, the manifest no
longer reads it, and a node whose engine wrote no key is named in the answer (declared_none) instead. The fallback
rows of the view test became the no-key and unreadable-engine cases; no test added.

(issue 774, 774/S7)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:12:53 +01:00
jschoubben fc5b849d85 The bus module sets and restores a key-value bucket's expiry (ADR 0582)
nats_bucket_expiry reads a bucket's stream, sets MaxAge (a duration, or none), reads it back, records a hand act
through mesh-hand-acts and says bucket-expiry-set, so the lease write can take the 15 seconds off the lease bucket and
the hand-back can restore them. Refuses a missing why and a name that is no bucket. Two new unit tests (stand-in
embedded server) in a module at 26 of 500; no test removed, none needed to stay in budget.

(issue 774, 774/S6)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 00:12:14 +01:00
mesh-tools ce3bedd584 Merge pull request 'pacman's mirror list keeps a fallback tail; a failed check leads the merge ask's headline (ADR 0586, issues 1876, 1166)' (#589) from fix/mirror-tail-headline into main 2026-11-01 18:04:05 +00:00
jschoubben 90f7f87c2a Say a failed check first in the merge ask's headline, since a merge is not gated on the check and the headline is the phone's only warning (issue 1166)
mesh/delivery merged: source.main moved to novox/mesh-catalog@ce3bedd584b8
mesh/check being carried to 90f7f87c2ac1 on ce3bedd584b8 | from 3483f603ae3d
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 18:06:27 +01:00
jschoubben 68b5febee7 Keep the freshest mirrors first and a tail of four more in pacman's shared list, so a node has a fallback (ADR 0586, issue 1876)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 18:05:26 +01:00
mesh-tools 3483f603ae Merge pull request 'pacman keeps one mirror list for the mesh and reads each node's own packages; the provisioner composes once (ADR 0585, ADR 0565 §6, ADR 0570; issues 1876, 1953, 1954)' (#588) from feat/node-packages into main 2026-11-01 17:02:26 +00:00
jschoubben 5ccce4967a Keep three timeout tests' upper bounds well under the production timeouts, so a timeout that falls back to production still fails (issue 1814, review round 3)
mesh/delivery-group group feat/node-packages merged: every member is merged
mesh/delivery merged: source.main moved to novox/mesh-catalog@3483f603ae3d
mesh/check checking 5ccce4967a98 on cff7588797b8 | novox/mesh-catalog#588 in group feat/node-packages
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 17:26:58 +01:00
jschoubben b3fcf408f2 Make tests that wait for concurrent work wait on the event or a condition with a long deadline (issue 1814)
A test that slept a fixed time and then asserted what a goroutine, process or
watcher did failed under the checker's load. Each now waits for its positive
evidence (a started baseline, a call counted, a state reached) within 30 s, and
the concurrency-limit tests meet at a barrier instead of overlapping by sleep.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 17:24:19 +01:00
jschoubben 3d1a450406 pacman starts a round's run only once the node's file holds the round's mirror list, and a refresh never writes after a round opened (ADR 0585 §2, §4; review round 1)
mesh/delivery-group group feat/node-packages rejected: composed together: the checker's verdict at the members' heads (98ce55efa892): unit: i3/cmd/i3-tools.Tes…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: i3/cmd/i3-tools.TestAtItsStartTheWatcherReloadsWhatTheRunningI3HasNotLoaded: loaded map[/tmp/TestAtItsStartTheWatcherReloadsW…
The opener ranked the list and took its turn in the same step, so its upgrade started
before the keeper's change hook wrote /etc/pacman.d/mirrorlist, and a failed write on
any node was only logged. A round's run now writes the list first and checks the file
holds the revision the round opened with; otherwise the run is refused with why and
tried again an hour later, as every refused run is. A refresh asks again for a running
round right before its write, since ranking takes minutes.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:32:59 +01:00
jschoubben 7375d613b4 Pin pacman and mesh-provisioner to the group's mesh-sdk head b2a27ed5 (issues 1953, 1876, 1954)
The delivery group feat/node-packages pins one mesh-sdk head in every member: pacman reads EnginePackagesRead from it, and mesh-provisioner composes a node once with it.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:24:04 +01:00
jschoubben 0471b2e105 Merge remote-tracking branch 'origin/main' into feat/node-packages 2026-11-01 16:20:59 +01:00
jschoubben 2f8cb338ff Merge branch 'feat/compose-once' of /home/jochen/projects/novox/mesh-catalog-compose into feat/node-packages 2026-11-01 16:19:54 +01:00
jschoubben a6c2f7b31a mesh-provisioner: compose a node once and read its own states from the copy (issue 1954)
provisioner_compose took 2.6-13.6 s per node with nothing changed, CPU
bound. The node is now composed at its wanted and its newest builds over
one read (frombus.PlansHeldTwice, whose composer resolves each machine
once), its own plans, provisions and refusals and the providers' entries
are read from the watched copy, its own writes taken in at once, and a
provider's manifest once by digest: no read by key per composition is
left. The result says where the time went, phase by phase, and a slow
composition that changed nothing says it in the log.

BenchmarkComposeNode times it over a mesh captured with the read tools
(MESH_COMPOSE_CAPTURE): novox 11.7 s to 0.49 s, ace 0.49 s, g14 and
shanks 0.39 s; plans and provisions identical to before over nine
compositions of the four nodes.

Needs mesh-sdk feat/compose-once (5d26c65); go.mod still pins the old
mesh-sdk until the group pins its head.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:19:13 +01:00
jschoubben 2b495c4818 pacman keeps one mirror list for the mesh, ranked in the write that opens each round and written by every node (ADR 0585, issue 1876)
Each node's reflector ranked its own list on its own timer, and on 2026-10-31 one node's
lagging first mirror left it a Node.js version behind the rest. The list is now the
module's state mirrors, key list: the node that opens a week's round ranks it with the
module's reflector options, keeps only mirrors whose last sync is no older than the
newest found, and the round's key names the list's revision. Every keeper writes
/etc/pacman.d/mirrorlist whole from the key through the root helper's new verb
mirrorlist, when the key changes and when the file differs, and never writes an empty
or unreadable list. reflector.timer is declared stopped and disabled, and reflector.conf
saves nothing. pacman_mirrors answers the shared list, its ranker and age and whether
this node's file matches; with refresh it asks this node's keeper to rank again,
refused while a round runs, naming it. The round's integration test proves the opening
write and both nodes' files.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:18:22 +01:00
jschoubben 37613ec6a9 pacman reads the node-engine's packages as mesh-host.packages, the bucket each engine writes (ADR 0565 clause 6, issue 1953)
The read node-engine.packages named a bucket node-engine_packages that nothing writes:
the engine's manifest is mesh-host's, so its state is mesh-host_packages, read as
disk-load reads mesh-host.data. The view still names which source answered, the
engine's key or the controller's mirror.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:09:50 +01:00
mesh-tools cff7588797 Merge pull request 'Agree a toolchain by strict majority; a node on another version sits out (ADR 0584, issue 1578)' (#587) from fix/toolchain-majority into main 2026-11-01 15:04:11 +00:00
jschoubben f06335cf7e Pin the Go and TypeScript build modules to mesh-sdk 67354f8: a check ask for another toolchain is declined too (ADR 0584, issue 1578)
mesh/delivery merged: source.main moved to novox/mesh-catalog@cff7588797b8
mesh/delivery-group group fix/toolchain-majority merged: every member is merged
mesh/check pass f06335cf7eff on fc300e3d7242 | judged by the candidate's mesh-build-go at 323bb9cdc526
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:38:27 +01:00
jschoubben c414d27c42 Pin the Go and TypeScript build modules to mesh-sdk 71be15f so a node off the agreed toolchain declines the ask (ADR 0584, issue 1578)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 15:28:57 +01:00
mesh-tools fc300e3d72 Merge pull request 'Readers leave the controller: hand acts, packages, compare, provisioner on the watched reader (design 47 L5-L7)' (#586) from feat/l5-l7-main into main 2026-11-01 13:53:20 +00:00
jschoubben 2002b08505 gitea: the check-owed test reads a missing consumes list as empty, since group A removed gitea's last consume
mesh/delivery merged: source.main moved to novox/mesh-catalog@fc300e3d7242
mesh/delivery-group group feat/l5-l7-main merged: every member is merged
mesh/check checking 2002b08505cc on fc300e3d7242 | novox/mesh-catalog#586 in group feat/l5-l7-main
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 10:39:03 +01:00
jschoubben 6de5c6680e Pin group A's mesh-sdk head on main
mesh/delivery-group group feat/l5-l7-main rejected: composed together: the checker's verdict at the members' heads (c598e176f3b2): unit: modules/gitea.the chec…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: modules/gitea.the checker's owed is what this module reads, and it consumes no check.status: "Cannot read properties of undef…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:57:46 +01:00
jschoubben 4a6395feae pacman falls back to the controller's mirror for every node when the engine's state cannot be read, and says the engine's error (ADR 0565 clause 6, review round 2)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:07 +01:00
jschoubben 1db0181920 pacman reads the node-engine's packages first and the controller's mirror per node until the lease write; compare counts only an indexer that was read, and a forge not heard is said in the row (review round 1)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:07 +01:00
jschoubben 8c07d2ef67 claude-code records its warrant through the hand-act seat, and the group's modules pin mesh-sdk at 8311ecb
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:07 +01:00
jschoubben a673fc52a7 mesh-config compare: leave out merged or closed pull requests' candidates and a module with no manifest and no mirrored keys, so compare can come out empty (issue 1953 L5d, L5e)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:07 +01:00
jschoubben b0dc7e08de mesh-provisioner recomposes a node when one of its haves changes, and pins mesh-sdk at 8311ecb
ADR 0580 clause 2 as revised: the holder hears haves by watch as it hears resolved values, so a have's change recomposes
its node from the copy it already holds.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:07 +01:00
jschoubben aa4a4253ad mesh-provisioner composes from one watched copy of its reads, not key by key
Composing one node read every placement, have, module and resolved key one at a time and took past 30 s with the bus
local (ADR 0580 clause 2): the holder now reads them from mesh-sdk's watched copy, its watch handlers queue the nodes a
change touches after the copy took it, and a resolved value's change recomposes its node. mesh-sdk pinned at ba8b8f2.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:54:07 +01:00
jschoubben cd7582a0f2 mesh-config and mesh-healers record into the hand-act seat alone, and gitea consumes no event of the controller (ADR 0565 clause 11, ADR 0571 clause 13, issues 1953 and 1955)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:53:59 +01:00
jschoubben 1d3e2b8aca pacman reads the node-engine's packages, not the controller's declared-packages mirror (ADR 0565 clause 6, issue 1953)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 09:53:59 +01:00
mesh-tools 462bcfe451 Merge pull request 'Catalog readers decode instead of judging; mesh-build-go builds against the asked package versions (ADR 0578, 0573 §2)' (#585) from feat/judge-once into main 2026-11-01 07:54:14 +00:00
jschoubben 432326264f artifact-store-tools: date the sweep test's builder log on the real clock
mesh/delivery merged: source.main moved to novox/mesh-catalog@462bcfe4510e
mesh/delivery-group group feat/judge-once merged: every member is merged
mesh/check checking 432326264f6c on 4b16c193f0fb | novox/mesh-catalog#585 in group feat/judge-once
The test copies aMesh's log, dated from 2026-10-01 for a fixed clock, but runs the holder on time.Now; since
2026-10-31 every entry fell outside the log's 30 days, so the sweep rightly kept every copy as unrecorded and the
mirror act was missing. The sweep is right; the fixture is shifted to now (issue 1955, L7a).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 03:40:56 +01:00
jschoubben 3e08301b27 Pin every module to the group's mesh-sdk head after the gofmt fix
mesh/delivery-group group feat/judge-once rejected: composed together: the checker's verdict at the members' heads (5c42ba1fb5f3): integration: artifactstoreto…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: artifactstoretools/cmd/artifa… | 3e08301b27ea on 4b16c193f0fb | judged by the candidate's mesh-build-go at 8dd51540f73a
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 03:30:59 +01:00
jschoubben c4406da4a3 Pin every module to the group's mesh-sdk head after review round 1
mesh/delivery-group group feat/judge-once checking: 6 of 6 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking c4406da4a3fe on 4b16c193f0fb | novox/mesh-catalog#585 in group feat/judge-once
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 03:17:47 +01:00
jschoubben c19edac804 Pin every module that names mesh-sdk at the group's final head, without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:50:43 +01:00
jschoubben fcd7df5356 Build Go modules against the package versions an ask names
ADR 0573 §2 and §4 have a build take the latest published version of
each mesh package, or a group's own head, whatever go.mod names (issue
1585, S15). The fetch writes each asked version into the directory's
temporary modfile before downloading: a trunk commit is required at its
Go version with its checksums for go mod verify, a pull request's head
is placed as a pin on a head is, a newer go.mod version is kept and
said, and the answer and the log name each.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:46:04 +01:00
jschoubben 9fa7f7b535 mesh-provisioner builds on the mesh-sdk without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben eed6e08000 mesh-delivery-gate builds on the mesh-sdk without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben 84264b4fd0 mesh-conditions builds on the mesh-sdk without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben e1799f3534 only-office builds on the mesh-sdk without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben baa2d93310 mesh-build-image builds on the mesh-sdk without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben 5b09353484 mesh-build-go builds on the mesh-sdk without the judge (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben fa88633427 mesh-build-node tests decode their manifest instead of judging it (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben 762982dee7 checks/manifests, credentialform and health build on the mesh-sdk without the judge
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben 021e641840 checks/sdkjudge lets the composer's placeholder and template refusals stay, and the real tree pass (ADR 0578 §5)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:56 +01:00
jschoubben d2b852c224 mesh-provisioner takes the indexer's verdicts through mesh-sdk, so a refused module is held, not fatal (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 2d9ba73221 mesh-delivery-gate tests decode their manifest instead of judging it (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben d8e2f9f6a6 mesh-conditions tests decode their manifest instead of judging it (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 7239b3e3dd only-office tests decode their manifest instead of judging it (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben a924397765 mesh-build-image decodes the manifest it builds from and judges none (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 9b12dfaf21 mesh-build-go decodes the manifest it builds from and judges none (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 7a143d1f22 Refuse a manifest judge in mesh-sdk beside a catalogue change: problem functions and allow-list tables (ADR 0578 §4)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 0bccd9f9e9 checks/health counts resources from decoded manifests instead of judging them (ADR 0578 §3)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 349a5774cd checks/credentialform stops judging credential forms, the checker's judge runs that rule (ADR 0578 §2)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
jschoubben 376d0211e9 checks/manifests decodes manifests and judges none, the checker's judge does (ADR 0578 §2)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:29:48 +01:00
491 changed files with 11223 additions and 5603 deletions
+3
View File
@@ -2,6 +2,7 @@ node_modules/
dist/
# Go tool bundles built in place (go build in a module's cmd/<name>-tools) are build output.
modules/pacman/pacman-tools
modules/slack/cmd/slack-tools/slack-tools
modules/jetbrains-toolbox/cmd/toolbox-tools/toolbox-tools
modules/messenger/cmd/messenger/messenger
@@ -41,3 +42,5 @@ modules/mesh-config/cmd/mesh-config/mesh-config
modules/forticlient/cmd/forticlient-tools/forticlient-tools
modules/networkmanager/uplink-tools
checks/words/words
modules/mesh-cli/mesh-cli
modules/mesh-cli/cmd/mesh-cli/mesh-cli
+138
View File
@@ -0,0 +1,138 @@
// Package controllerdeps is the catalogue's dependence check on the controller (novox/hq ADR 0590 §5, issue 1126): the
// controller is not placed, so no module of this tree reads, requires, invokes, consumes, waits on, uses or otherwise
// names it. Nothing is kept: not the lease, not the entries once kept to its write (ADR 0571 §13, superseded with ADR
// 0582). A later decision that places a controller again must first lift this check.
//
// It refuses, over the tree at root:
//
// - a directory modules/mesh-controller: the controller is no module of the catalogue;
// - a manifest string naming the controller's seat, a state, verb or event of it, or one of its buckets
// (`mesh-controller.<name>`, `seat:mesh-controller.<verb>`, `mesh-controller_<bucket>`), anywhere in the manifest;
// - a manifest string that is the bare module name `mesh-controller` outside the module's own `config`, where a value
// is the module's data (an issue owner naming the controller's repository is no dependency), not an address;
// - a line of any module's file naming one of the controller's buckets (`mesh-controller_`): its buckets are removed
// after the controller's end (ADR 0590 §6), so a reader of one reads nothing. A test (`_test.go`, `.test.ts`) reads
// no live bucket, its names are its own data, and a `testdata` directory is a copy of text from elsewhere (a design,
// a backlog): both are skipped.
//
// The rule is said once, here, for the catalogue: decoding stays the manifests check's (a manifest that is no JSON is
// an error here, never judged).
package controllerdeps
import (
"bufio"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// Module is the controller's module name, and Bucket the prefix every bucket of its carries.
const (
Module = "mesh-controller"
Bucket = Module + "_"
)
// address is a string naming the controller's seat, one of its states, verbs or events, or one of its buckets.
var address = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller([._][A-Za-z0-9])`)
// Refusals is each reason the check gives over the tree at root, sorted; none when it passes.
func Refusals(root string) ([]string, error) {
var out []string
if _, err := os.Lstat(filepath.Join(root, "modules", Module)); err == nil {
out = append(out, Module+": the controller is no module of the catalogue: it is not placed and nothing may depend on it (novox/hq ADR 0590 §5)")
}
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
return nil, err
}
var doc any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, fmt.Errorf("%s: %w", f, err)
}
module := filepath.Base(filepath.Dir(f))
for _, s := range names(doc, false, nil) {
out = append(out, fmt.Sprintf("%s: names %q: the controller is not placed, so no manifest reads, requires, invokes, "+
"consumes, waits on or uses it; name the seat that holds the job now (novox/hq ADR 0590 §5)", module, s))
}
}
buckets, err := bucketLines(root)
if err != nil {
return nil, err
}
out = append(out, buckets...)
sort.Strings(out)
return out, nil
}
// names collects the strings of a decoded manifest that name the controller; inConfig is under the module's `config`.
func names(v any, inConfig bool, into []string) []string {
switch v := v.(type) {
case string:
if address.MatchString(v) || (!inConfig && strings.TrimSpace(v) == Module) {
into = append(into, v)
}
case []any:
for _, e := range v {
into = names(e, inConfig, into)
}
case map[string]any:
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
into = names(v[k], inConfig || k == "config", into)
}
}
return into
}
// bucketLines is every line of a module's file, outside tests and testdata, that names one of the controller's buckets.
func bucketLines(root string) ([]string, error) {
var out []string
modules := filepath.Join(root, "modules")
err := filepath.WalkDir(modules, func(path string, d fs.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) && path == modules {
return nil
}
return err
}
if d.IsDir() {
if n := d.Name(); n == "testdata" || n == "node_modules" || n == ".git" {
return filepath.SkipDir
}
return nil
}
if !d.Type().IsRegular() || strings.HasSuffix(d.Name(), "_test.go") || strings.HasSuffix(d.Name(), ".test.ts") {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
rel, _ := filepath.Rel(root, path)
for n := 1; sc.Scan(); n++ {
if strings.Contains(sc.Text(), Bucket) {
out = append(out, fmt.Sprintf("%s:%d: names the controller's bucket (%s…): its buckets are removed after its "+
"end (novox/hq ADR 0590 §6); read the seat that holds the job now", filepath.ToSlash(rel), n, Bucket))
}
}
return sc.Err()
})
return out, err
}
@@ -0,0 +1,61 @@
package controllerdeps
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ADR 0590 §5 (issue 1126 B10): no manifest of the catalogue at this head reads, invokes, consumes, waits on, uses or
// otherwise names the controller's seat, states or buckets, and no module's code names a bucket of the controller's.
func TestNothingInTheCatalogueNamesTheController(t *testing.T) {
got, err := Refusals("../..")
if err != nil {
t.Fatal(err)
}
for _, r := range got {
t.Error(r)
}
}
// The check refuses every name of the controller, the lease and the entries once kept to its write among them, on a
// tree of its own; a repository named mesh-controller (an issue's owner) and a word in prose are no dependency.
func TestRefusalsJudgeATree(t *testing.T) {
for _, row := range []struct {
name string
files map[string]string
want string // empty: passes
}{
{"the lease is refused like any read", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.lease"]}`}, `a: names "mesh-controller.lease"`},
{"a verb once kept to the write", map[string]string{"modules/b/module.json": `{"module":"b","invokes":["seat:mesh-controller.collect"]}`}, `b: names "seat:mesh-controller.collect"`},
{"an event consumed", map[string]string{"modules/c/module.json": `{"module":"c","consumes":[{"event":"mesh-controller.artifacts-gone"}]}`}, "mesh-controller.artifacts-gone"},
{"a requirement", map[string]string{"modules/d/module.json": `{"module":"d","requires":[{"name":"x","from":"mesh-controller"}]}`}, `d: names "mesh-controller"`},
{"the controller's own module", map[string]string{"modules/mesh-controller/module.json": `{"module":"mesh-controller"}`}, "mesh-controller: the controller is no module of the catalogue"},
{"a bucket in code", map[string]string{"modules/e/cmd/e/main.go": "package main\nconst b = \"mesh-controller_assignments\"\n"}, "modules/e/cmd/e/main.go:2: names the controller's bucket"},
{"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""},
{"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""},
{"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""},
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-placement.placement"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
for path, text := range row.files {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Refusals(root)
if err != nil {
t.Fatal(err)
}
if joined := strings.Join(got, "\n"); (row.want == "") != (joined == "") || !strings.Contains(joined, row.want) {
t.Errorf("refusals %q, want %q", joined, row.want)
}
})
}
}
+3
View File
@@ -0,0 +1,3 @@
module checks/controllerdeps
go 1.22
+3
View File
@@ -0,0 +1,3 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module's directory: its manifest, for a name of the controller, and its files, for one of the controller's buckets.
modules
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/credentialform
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+9 -7
View File
@@ -1,6 +1,8 @@
// Package credentialform refuses an offer that does not say the form of its credential: an own secret, an issuer, or
// `form` pair or none (the lead, 2026-10-29). It reads every modules/*/module.json through mesh-sdk's
// CredentialFormProblems.
// Package credentialform refuses what the catalogue's tree holds about credentials and routes that no manifest judge
// sees: a module that provides and keeps a `bound` state the provisioner does not read, and a route received as a file.
// It judges no offer's credential form: that rule (an own secret, an issuer, or `form` pair or none; the lead,
// 2026-10-29) is part of the catalogue's rules, which the checker's judge runs on every change (CatalogueProblems,
// novox/hq ADR 0578 §2); this package called it itself until 2026-11.
//
// It refuses a route received as a file too (issue 774, research 072 row 34, the operator on 2026-10-31: "there is only
// 1 path, the other is simply deprecated/invalid"): a route is a provision, served from the seat mesh-provisioner's
@@ -20,7 +22,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Problems is every offer of the manifests under root/modules that says no credential form, one line each.
// Problems is every module of the manifests under root/modules that keeps a `bound` state the provisioner does not read,
// and every route received as a file, one line each.
func Problems(root string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
@@ -36,11 +39,10 @@ func Problems(root string) ([]string, error) {
if err != nil {
return nil, err
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
m, err := catalogue.Decode(raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", f, err)
}
out = append(out, catalogue.CredentialFormProblems(m)...)
if _, received := m.Receives["route"]; received {
receiving = append(receiving, m.Module)
} else if _, listed := stillReceivingRoutes[m.Module]; listed {
+8 -8
View File
@@ -22,9 +22,10 @@ func aCatalogue(t *testing.T, manifests map[string]string) string {
return root
}
// An offer that says no form is refused and named; one that says pair, none or an own secret passes; a route received
// as a file is refused but for the module still moving off it; and the real catalogue is clean.
func TestEveryOfferSaysTheFormOfItsCredential(t *testing.T) {
// A binder the provisioner does not read is refused and named; an offer that says no form is the judge's to refuse, not
// this check's; a route received as a file is refused but for the module still moving off it; and the real catalogue is
// clean.
func TestNoBinderIsUnreadAndNoRouteIsReceivedAsAFile(t *testing.T) {
root := aCatalogue(t, map[string]string{
"silent": `{"module":"silent","provides":[{"name":"thing","scope":"mesh"}]}`,
"binder": `{"module":"binder","provides":[{"name":"d","credential":{"form":"pair"}}],"state":["bound"]}`,
@@ -39,16 +40,15 @@ func TestEveryOfferSaysTheFormOfItsCredential(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(got) != 3 || !strings.Contains(got[0], `silent provides "thing"`) ||
!strings.Contains(got[1], "binder") || !strings.Contains(got[1], "mesh-provisioner") ||
!strings.HasPrefix(got[2], "old-proxy receives route") || !strings.Contains(got[2], "old-proxy, route-adapter") {
t.Fatalf("refused %v, wanted the silent offer, the binder the provisioner does not read and the received route", got)
if len(got) != 2 || !strings.Contains(got[0], "binder") || !strings.Contains(got[0], "mesh-provisioner") ||
!strings.HasPrefix(got[1], "old-proxy receives route") || !strings.Contains(got[1], "old-proxy, route-adapter") {
t.Fatalf("refused %v, wanted the binder the provisioner does not read and the received route", got)
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) != 0 {
t.Fatalf("the catalogue has offers with no credential form:\n%s", strings.Join(real, "\n"))
t.Fatalf("the catalogue has binders or routes it must not:\n%s", strings.Join(real, "\n"))
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for what each offer says of its credential's form.
# every manifest, for its bound state and its received routes.
modules/*/module.json
-8
View File
@@ -1,8 +0,0 @@
# The calls of the controller's hand-act log a manifest of this catalogue may still invoke, each `<module> <verb> <why>`:
# the fallback of a recorder (mesh-sdk handacts.Recorder) while the seat mesh-hand-acts has no holder that answers, until
# the controller's lease is written to a holder that is no controller. The check of this directory (handacts) refuses any
# other, and an entry whose module no longer invokes it: the list only shrinks, and it is empty before the lease write
# (novox/hq ADR 0565 §11; issue 1953, issue 774).
mesh-config hand-act fallback until the lease write: its sets, clears and held values record through mesh-hand-acts first
mesh-config warranted fallback until the lease write: its warrants record through mesh-hand-acts first
mesh-healers drill fallback until the lease write: a drill records through mesh-hand-acts first
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/handacts
go 1.22
-109
View File
@@ -1,109 +0,0 @@
// Command handacts refuses a manifest that records into the controller's hand-act log (novox/hq ADR 0565 §11): every
// recorder records through the seat mesh-hand-acts from its delivery, so no manifest invokes
// `seat:mesh-controller.hand-act`, `.warranted` or `.drill`. The recorders' fallbacks to the controller's log while the
// seat has no holder are named in checks/handacts/fallbacks, each module and verb with why; that list only shrinks, and
// an entry whose module no longer invokes the verb is refused too.
//
// go run . <catalog root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Verbs are the controller's hand-act verbs, as a manifest invokes them, and Allowed the list of named fallbacks.
var Verbs = []string{"hand-act", "warranted", "drill"}
const (
invokePrefix = "seat:mesh-controller."
Allowed = "checks/handacts/fallbacks"
)
// Findings is every refusal in the catalog at root, sorted.
func Findings(root string) ([]string, error) {
allowed := map[string]bool{}
raw, err := os.ReadFile(filepath.Join(root, Allowed))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) >= 3 && !strings.HasPrefix(f[0], "#") {
allowed[f[0]+" "+f[1]] = true
} else if len(f) > 0 && !strings.HasPrefix(f[0], "#") {
return nil, fmt.Errorf("%s: %q is not `<module> <verb> <why>`", Allowed, line)
}
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
var out []string
invoked := map[string]bool{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Invokes []string `json:"invokes"`
}
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
continue
}
for _, inv := range m.Invokes {
verb, ok := strings.CutPrefix(strings.TrimSpace(inv), invokePrefix)
if !ok || !contains(Verbs, verb) {
continue
}
invoked[m.Module+" "+verb] = true
if !allowed[m.Module+" "+verb] {
out = append(out, fmt.Sprintf("%s invokes %s%s: record through seat:mesh-hand-acts.%s (novox/hq ADR 0565 §11), "+
"or name the fallback in %s", m.Module, invokePrefix, verb, verb, Allowed))
}
}
}
for entry := range allowed {
if !invoked[entry] {
out = append(out, fmt.Sprintf("%s names %q, which is no longer invoked: take it off the list, which only shrinks",
Allowed, entry))
}
}
sort.Strings(out)
return out, nil
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: handacts <catalog root>")
os.Exit(2)
}
found, err := Findings(os.Args[1])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no manifest records into the controller's hand-act log but the fallbacks named")
}
-59
View File
@@ -1,59 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// No manifest invokes the controller's hand-act verbs but a named fallback; a named fallback no longer invoked is
// refused (novox/hq ADR 0565 §11's catalog test).
func TestFindings(t *testing.T) {
for _, row := range []struct {
name string
files map[string]string
want string // empty: clean
}{
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
{"an unnamed call", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.warranted"]}`},
"a invokes seat:mesh-controller.warranted"},
{"a named fallback, and the controller's own", map[string]string{
"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.drill"]}`,
"modules/mesh-controller/module.json": `{"module":"mesh-controller","invokes":["seat:mesh-controller.hand-act"]}`,
Allowed: "# why\na drill fallback until the lease write\n"}, ""},
{"a fallback no longer invoked", map[string]string{Allowed: "gone hand-act fallback\n"}, `names "gone hand-act"`},
{"another verb of the controller", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.conditions"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
for path, text := range row.files {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Findings(root)
if err != nil {
t.Fatal(err)
}
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
t.Fatalf("findings = %q, want one containing %q", all, row.want)
}
})
}
}
// The run of this check over the real tree.
func TestTheCatalogRecordsOnlyThroughTheSeat(t *testing.T) {
got, err := Findings(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, f := range got {
t.Error(f)
}
}
-3
View File
@@ -1,3 +0,0 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for what it invokes.
modules/*/module.json
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/health
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+8 -3
View File
@@ -1,7 +1,8 @@
// Package health is the catalogue's count of long-running resources that do not say how they are ready (novox/hq ADR 0240 rule
// 8), kept in health-undeclared and judged by a test over the real tree (ADR 0529 clauses 2 and 4): it replaces the step of
// merge-check.sh that read it out of the controller's `module check`. Every manifest is read through mesh-sdk's catalogue
// package, the one the controller counts with.
// merge-check.sh that read it out of the controller's `module check`. Every manifest is decoded with mesh-sdk's catalogue
// package, the one the controller counts with, and judged by nothing here (novox/hq ADR 0578 §3): a resource's `health` is
// in the manifest itself, not in a template's text.
package health
import (
@@ -27,7 +28,11 @@ func Count(root string) (int, error) {
}
n := 0
for _, path := range manifests {
m, err := catalogue.ReadTreeManifest(root, filepath.Dir(path))
raw, err := os.ReadFile(path)
if err != nil {
return 0, err
}
m, err := catalogue.Decode(raw)
if err != nil {
return 0, fmt.Errorf("%s: %w", path, err)
}
+1 -5
View File
@@ -1,10 +1,6 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# every manifest through mesh-sdk's ReadTreeManifest: the root's predefined keys and each templated module's
# templates (files/ by default, or the directory its manifest names); and the root's kept count of resources that do not say how they are ready.
# every manifest, decoded; and the root's kept count of resources that do not say how they are ready.
modules/*/module.json
predefined.json
modules/*/files
modules/*/templates
health-undeclared
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/manifests
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+14 -27
View File
@@ -1,13 +1,11 @@
// Package manifests judges every module's manifest as the node-engine reads it, and the catalogue as one shelf: each
// modules/*/module.json through mesh-sdk's ParseManifestWith against the predefined.json at the root, with its templates
// read from the tree beside it (WithTemplatesFrom), then the rules between manifests (CatalogueProblems,
// IdentityProblems, DataProblems).
// Package manifests reads every module's manifest as the catalogue's tree holds it and refuses what no judge could
// read: a module.json that does not decode, and two manifests that name one module.
//
// Why it is here (issue 774): on 2026-10-30 mesh-catalog ca8d6f33 gave power's bar item two attributes its bar places
// by none of. ParseManifest refuses that, and every node-engine refused to compose at 19:13; no check of the pull
// request had parsed the manifest. That judgement was mesh-controller's `module check`, which the merge gate ran over
// every manifest before the checker moved to mesh-delivery; the checker runs this repository's Go tests and the
// composition, and no test parsed a manifest whole. This one does, in the same functions the nodes use.
// It judges no manifest (novox/hq ADR 0578 §2): the checker judges every change's manifests with the indexer's judge, the
// one judge, so the field, seat, bus and catalogue rules are said once, there. Until 2026-11 this check parsed each
// manifest with mesh-sdk's strict parser and ran the rules between manifests (issue 774, power's bar item of
// mesh-catalog ca8d6f33); that judgement moved to the checker, and what stays is the two refusals that are about the
// tree, not a manifest's rules.
package manifests
import (
@@ -19,43 +17,32 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Problems is what is wrong with the manifests under root/modules, one line each: a manifest the mesh refuses, named by
// its path, then the rules between the manifests that parse.
// Problems is what is wrong with the tree's manifests under root/modules, one line each: a module.json whose bytes are
// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), then a module named by two manifests.
func Problems(root string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
list, err := catalogue.ReadPredefined(root)
if err != nil {
return nil, err
}
var out []string
shelf := catalogue.Shelf{}
named := map[string]bool{}
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
return nil, err
}
rel, _ := filepath.Rel(root, f)
m, err := catalogue.ParseManifestWith(raw, list)
if err == nil {
m, err = catalogue.WithTemplatesFrom(m, filepath.Dir(f))
}
m, err := catalogue.Decode(raw)
if err != nil {
out = append(out, fmt.Sprintf("%s: %v", rel, err))
continue
}
if _, twice := shelf[m.Module]; twice {
if named[m.Module] {
out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module))
continue
}
shelf[m.Module] = m
named[m.Module] = true
}
between := catalogue.CatalogueProblems(shelf)
between = append(between, catalogue.IdentityProblems(shelf, catalogue.DefaultLongestMachine)...)
between = append(between, catalogue.DataProblems(shelf)...)
sort.Strings(between)
return append(out, between...), nil
return out, nil
}
+18 -19
View File
@@ -7,28 +7,27 @@ import (
"testing"
)
// Every manifest of the catalogue is one the nodes can use, and the catalogue holds together as one shelf. A row is a
// manifest written into a catalogue of its own: power's bar item as mesh-catalog ca8d6f33 gave it (issue 774) is
// refused naming the attribute, and the same item without it passes.
func TestEveryManifestIsOneTheNodesCanUse(t *testing.T) {
// The tree's manifests are ones a judge can read, and no module is named twice. A row is a catalogue of its own: a
// manifest that is no JSON and two manifests of one module are refused, naming the file; a manifest with a field this
// check does not know is not (it judges nothing: the checker does, ADR 0578 §2). The catalogue itself passes.
func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) {
for _, row := range []struct {
name, attributes, refused string
name string
files map[string]string
refused string
}{
{name: "the bar places by bar and place", attributes: `"bar":"bottom","place":"status","order":"45"`},
{name: "an attribute the bar places by none of (ca8d6f33)",
attributes: `"bar":"bottom","place":"status","order":"45","shows":"state","state":"power.draw"`,
refused: `"shows" is none of them`},
{name: "a manifest the judge can read", files: map[string]string{"a": `{"module":"a","version":"1"}`}},
{name: "a field no part knows is the judge's to refuse", files: map[string]string{"a": `{"module":"a","version":"1","unheard":true}`}},
{name: "bytes that are no manifest", files: map[string]string{"a": `{"module":`}, refused: "modules/a/module.json"},
{name: "two manifests of one module", files: map[string]string{"a": `{"module":"x","version":"1"}`, "b": `{"module":"x","version":"1"}`},
refused: "two manifests name one module"},
} {
root := t.TempDir()
dir := filepath.Join(root, "modules", "bar-user")
if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil {
t.Fatal(err)
}
manifest := `{"module":"bar-user","version":"1","moved":true,` +
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
`"items":[{"collection":"contribution:node-bar:block","attributes":{` + row.attributes + `},"template":"files/bar"}]}`
for path, body := range map[string]string{"module.json": manifest, "files/bar": "{\"shows\":\"command\"}\n"} {
if err := os.WriteFile(filepath.Join(dir, path), []byte(body), 0o644); err != nil {
for dir, manifest := range row.files {
if err := os.MkdirAll(filepath.Join(root, "modules", dir), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "modules", dir, "module.json"), []byte(manifest), 0o644); err != nil {
t.Fatal(err)
}
}
@@ -46,6 +45,6 @@ func TestEveryManifestIsOneTheNodesCanUse(t *testing.T) {
t.Fatal(err)
}
if len(real) != 0 {
t.Fatalf("the catalogue holds manifests the nodes cannot use:\n%s", strings.Join(real, "\n"))
t.Fatalf("the catalogue holds manifests no judge can read:\n%s", strings.Join(real, "\n"))
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module's directory (its manifest and the templates beside it), and the list of predefined keys.
# every module's directory (its manifest), decoded.
modules
predefined.json
-4
View File
@@ -1,4 +0,0 @@
# The modules of this catalogue that still read the controller's assignments (mesh-controller.assignments), each with
# why. The check of this directory (noplacement) refuses any other module that reads them, and an entry here whose module no longer
# does: the list only shrinks, and it is empty before the controller's lease is written to a holder that is no
# controller (novox/hq ADR 0549 §3, §6; issue 774).
-4
View File
@@ -1,7 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/noplacement
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+3 -70
View File
@@ -10,11 +10,8 @@
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`;
//
// 4. a manifest, other than mesh-controller's, that reads the controller's assignments (`mesh-controller.assignments`):
// every reader takes placement from the seat mesh-placement (novox/hq ADR 0549 §3, ADR 0567), and the lease write is
// refused while one still reads them (ADR 0549 §6; issue 774). The modules that still read them for what the
// placement does not carry are named in checks/noplacement/assignments-readers, each with why; that list only
// shrinks: an entry whose module no longer reads them is refused too.
// The controller's assignments, which item 4 once listed readers of, are refused with every other name of the
// controller by checks/controllerdeps (novox/hq ADR 0590 §5).
//
// go run . <catalogue root>
//
@@ -61,74 +58,10 @@ func Findings(root string) ([]string, error) {
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
}
}
readers, err := assignmentsReaders(root)
if err != nil {
return nil, err
}
out = append(out, readers...)
sort.Strings(out)
return out, nil
}
// AssignmentsRead is the controller's assignments as a manifest reads them, and Allowed the file, relative to the
// catalogue's root, that names the modules still reading them: `<module> <why>` per line, `#` a comment.
const (
AssignmentsRead = "mesh-controller.assignments"
Allowed = "checks/noplacement/assignments-readers"
)
// assignmentsReaders is every finding on the reads of the controller's assignments: a module other than the controller
// reading them that Allowed does not name, and a module Allowed names that no longer reads them (issue 774).
func assignmentsReaders(root string) ([]string, error) {
allowed := map[string]bool{}
raw, err := os.ReadFile(filepath.Join(root, Allowed))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
allowed[f[0]] = true
}
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
var found []string
reads := map[string]bool{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Reads []string `json:"reads"`
}
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
continue // a manifest that does not read is the manifest checks' to refuse
}
for _, r := range m.Reads {
if strings.TrimSpace(r) != AssignmentsRead {
continue
}
reads[m.Module] = true
if !allowed[m.Module] {
found = append(found, fmt.Sprintf("%s reads %s: read where modules run from mesh-delivery-assigner.placement, "+
"the seat mesh-placement's (novox/hq ADR 0549 §3, ADR 0567); the lease write is refused while a module reads "+
"the assignments (ADR 0549 §6)", m.Module, AssignmentsRead))
}
}
}
for module := range allowed {
if !reads[module] {
found = append(found, fmt.Sprintf("%s names %s, which no longer reads %s: take it off the list, which only shrinks",
Allowed, module, AssignmentsRead))
}
}
return found, nil
}
// named collects the strings of a decoded manifest that start with one of the Prefixes.
func named(v any, into []string) []string {
switch v := v.(type) {
@@ -166,5 +99,5 @@ func main() {
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no placement directory, no manifest reads the indexer's placement, and only the modules listed read the controller's assignments")
fmt.Println("no placement directory, and no manifest reads the indexer's placement")
}
-33
View File
@@ -5,8 +5,6 @@ import (
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
@@ -21,12 +19,6 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
// Issue 774 (ADR 0549 §3, §6): a reader of the controller's assignments is refused unless the list names it, the
// controller's own manifest is not judged, and a name on the list whose module no longer reads them is refused.
{"an unlisted reader of the assignments", map[string]string{"modules/r/module.json": `{"module":"r","reads":["mesh-controller.assignments"]}`}, "r reads mesh-controller.assignments"},
{"a listed reader and the controller", map[string]string{"modules/l/module.json": `{"module":"l","reads":["mesh-controller.assignments"]}`,
"modules/mesh-controller/module.json": `{"module":"mesh-controller","reads":["mesh-controller.assignments"]}`, Allowed: "# why\nl its plan\n"}, ""},
{"a listed module that reads them no more", map[string]string{Allowed: "gone no longer reads\n"}, "names gone, which no longer reads"},
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
@@ -60,29 +52,4 @@ func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
for _, f := range got {
t.Error(f)
}
// Issue 774 (ADR 0549 §3, §6; the comparison of novox/hq ADR 0529 clause 2): the list of modules still reading the
// controller's assignments gains none the trunk's copy at the merge base does not hold. Its first copy gains nothing.
path, ok := trunk.Copy(t, Allowed)
if !ok {
return
}
names := func(path string) map[string]bool {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
out := map[string]bool{}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
out[f[0]] = true
}
}
return out
}
base := names(path)
for module := range names(filepath.Join("..", "..", Allowed)) {
if !base[module] {
t.Errorf("%s gains %s, which the trunk's copy does not hold: the list only shrinks", Allowed, module)
}
}
}
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/readable
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/sdkjudge
go 1.22
+189
View File
@@ -0,0 +1,189 @@
// Package sdkjudge refuses mesh-sdk a manifest judge (novox/hq ADR 0578 §4): a package every module compiles in carries
// the contract only, so it holds no function that gathers a manifest's problems and no table of what a manifest may
// declare. The judge is a package of mesh-delivery that only the indexer and the checker use, and a reader decodes a
// manifest and takes the indexer's verdict (ADR 0578 §3).
//
// It reads mesh-sdk's source as the checker places it beside this check (MESH_CHECK_BESIDE, the repository mesh-sdk: at its
// trunk, or at the head of the group's pull request when the group holds one), so a change adding either to mesh-sdk in a
// delivery group with a catalogue change is refused here. A change to mesh-sdk alone is checked by mesh-sdk's own check,
// which does not run this package: the catalogue's check sees the SDK only beside a catalogue change.
//
// What it refuses, in the non-test Go files of mesh-sdk's go/ tree:
// - a function or method named for problems (a name ending in Problems or Problem) that takes a Manifest or a Shelf, and
// a function named ParseManifest, ParseManifestWith, ParseManifestForInputs, ValidateManifest... or ValidateShelf...: the
// judge's entry points;
// - in the package catalogue, a package-level variable named for what is allowed, permitted, removable, reserved or
// forbidden whose value is a map or a list: an allow-list table.
//
// The composer's refusals of composition itself (what a machine can be composed with) are not a judgement of a manifest
// (ADR 0578 §4, clause 5; issue 1585 tracks moving them): they are named in composition, each with why, and the list only
// shrinks, since a name that no longer exists is refused too.
package sdkjudge
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// composition is every function or table the check lets stay in mesh-sdk, as "path-from-go/:name", and why. It names no
// manifest judgement: each is the composer's refusal of composition itself (ADR 0578 clause 5).
var composition = map[string]string{
"catalogue/consumers.go:consumersPlaceholderProblems": "composition: rendering a consumer's template",
"catalogue/collections.go:collectedPlaceholderProblems": "composition: placeholders a contribution renders",
"catalogue/environment_into.go:placeholderProblems": "composition: placeholders a contribution renders",
"catalogue/seat_contributions.go:seatPlaceholderProblems": "composition: placeholders a contribution renders",
"catalogue/collections.go:itemTemplateProblem": "composition: filling an item's template",
"catalogue/setting_takes.go:takenSettingsProblems": "composition: whether stored settings still compose",
"catalogue/template.go:HeldTemplateProblem": "composition: the take-in refusal of a resolved build",
}
// Problems is what mesh-sdk's Go tree under root (its go/ directory) holds of a manifest judge, one line each, but for
// what composition lets stay (the map the caller gives; the test of the real tree gives the package's), and the
// entries of composition it no longer holds.
func Problems(root string, composition map[string]string) ([]string, error) {
var out []string
seen := map[string]bool{}
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); n == "testdata" || n == "vendor" || strings.HasPrefix(n, ".") {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
rel, _ := filepath.Rel(root, path)
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
if err != nil {
return fmt.Errorf("%s: %w", rel, err)
}
inCatalogue := filepath.Base(filepath.Dir(path)) == "catalogue"
for _, d := range file.Decls {
switch d := d.(type) {
case *ast.FuncDecl:
if why := judgeFunc(d); why != "" {
out = append(out, note(composition, seen, rel, d.Name.Name, why))
}
case *ast.GenDecl:
if !inCatalogue || d.Tok != token.VAR {
continue
}
for _, spec := range d.Specs {
v, _ := spec.(*ast.ValueSpec)
if v == nil {
continue
}
for i, name := range v.Names {
if i < len(v.Values) && allowName.MatchString(name.Name) && isTable(v.Values[i]) {
out = append(out, note(composition, seen, rel, name.Name,
"a table of what a manifest may declare (an allow-list)"))
}
}
}
}
}
return nil
})
if err != nil {
return nil, err
}
var kept []string
for _, line := range out {
if line != "" {
kept = append(kept, line)
}
}
for key, why := range composition {
if !seen[key] {
kept = append(kept, fmt.Sprintf("composition lists %s (%s), which mesh-sdk no longer holds: take it off the list", key, why))
}
}
sort.Strings(kept)
return kept, nil
}
// note is the line refusing name in rel for why, or "" when composition lets it stay.
func note(composition map[string]string, seen map[string]bool, rel, name, why string) string {
key := rel + ":" + name
if _, ok := composition[key]; ok {
seen[key] = true
return ""
}
return fmt.Sprintf("%s: %s is %s; mesh-sdk carries the contract only, the judge is the indexer's and the checker's (ADR 0578 §4)", rel, name, why)
}
// allowName is what names a table of what is allowed.
var allowName = regexp.MustCompile(`(?i)allow|permit|removable|reserved|forbid|denied`)
// isTable says whether an expression is a map or a list literal of more than one entry.
func isTable(e ast.Expr) bool {
c, ok := e.(*ast.CompositeLit)
if !ok || len(c.Elts) < 2 {
return false
}
switch c.Type.(type) {
case *ast.MapType, *ast.ArrayType:
return true
}
return false
}
// judgeFunc says why a function is a manifest judge, or "".
func judgeFunc(f *ast.FuncDecl) string {
name := f.Name.Name
if judgeEntry.MatchString(name) {
return "a function that judges a manifest"
}
if !strings.HasSuffix(name, "Problems") && !strings.HasSuffix(name, "Problem") {
return ""
}
if takes(f.Recv) || takes(f.Type.Params) {
return "a function that gathers a manifest's problems"
}
return ""
}
// judgeEntry is the names of the judge's entry points: the strict parsers and the validators of a manifest or a shelf.
var judgeEntry = regexp.MustCompile(`^(ParseManifest(With|ForInputs)?|Validate(Manifest|Shelf)\w*)$`)
// takes says whether a list of parameters names a Manifest or a Shelf anywhere in a type.
func takes(fields *ast.FieldList) bool {
if fields == nil {
return false
}
found := false
for _, f := range fields.List {
ast.Inspect(f.Type, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok && (id.Name == "Manifest" || id.Name == "Shelf") {
found = true
}
return !found
})
}
return found
}
// BesideDir is where the checker places mesh-sdk's clone, as a directory under MESH_CHECK_BESIDE; its go/ tree is judged.
func BesideDir() (string, error) {
beside := os.Getenv("MESH_CHECK_BESIDE")
if beside == "" {
return "", fmt.Errorf("NOT CHECKED: MESH_CHECK_BESIDE is not set, so mesh-sdk's source is not beside this check; the checker places the SDK there (besideRules)")
}
dir := filepath.Join(beside, "mesh-sdk", "go")
if _, err := os.Stat(dir); err != nil {
return "", fmt.Errorf("NOT CHECKED: mesh-sdk is not beside this check in MESH_CHECK_BESIDE=%s: %w", beside, err)
}
return dir, nil
}
+66
View File
@@ -0,0 +1,66 @@
package sdkjudge
import (
"os"
"path/filepath"
"strings"
"testing"
)
// mesh-sdk carries the contract only (ADR 0578 §4). A row is a source tree of its own: a function gathering a manifest's
// problems, a manifest parser that judges and a table of what a manifest may declare are refused, named; a Problems
// function over something that is no manifest, a test file, a table outside the catalogue package and a function that
// merely decodes are not. Then the SDK beside this check is judged.
func TestMeshSDKCarriesNoManifestJudge(t *testing.T) {
for _, row := range []struct {
name, dir, file, src, refused string
}{
{name: "a function gathering a manifest's problems", dir: "catalogue", refused: "StreamProblems is a function that gathers a manifest's problems",
src: "package catalogue\ntype Manifest struct{}\nfunc StreamProblems(m Manifest) []string { return nil }\n"},
{name: "a method over a manifest", dir: "catalogue", refused: "healthProblems is a function that gathers",
src: "package catalogue\ntype Manifest struct{}\nfunc (m Manifest) healthProblems() []string { return nil }\n"},
{name: "a function over the shelf", dir: "catalogue", refused: "CatalogueProblems is a function that gathers",
src: "package catalogue\ntype Shelf map[string]int\nfunc CatalogueProblems(s Shelf) []string { return nil }\n"},
{name: "a parser that judges", dir: "catalogue", refused: "ParseManifestWith is a function that judges a manifest",
src: "package catalogue\nfunc ParseManifestWith(raw []byte) error { return nil }\n"},
{name: "a table of what may be removed", dir: "catalogue", refused: "removable is a table of what a manifest may declare",
src: "package catalogue\nvar removable = map[string]bool{\"A\": true, \"B\": true}\n"},
{name: "a key parser", dir: "delivery", src: "package delivery\nfunc ParseManifestsKey(k string) error { return nil }\n"},
{name: "a decoder", dir: "catalogue", src: "package catalogue\ntype Manifest struct{}\nfunc Decode(raw []byte) (Manifest, error) { return Manifest{}, nil }\n"},
{name: "problems of something else", dir: "asks", src: "package asks\ntype Condition struct{}\nfunc DecodeProblems(c Condition) []string { return nil }\n"},
{name: "a table beside the catalogue", dir: "asks", src: "package asks\nvar allowed = []string{\"a\", \"b\"}\n"},
{name: "a test file", dir: "catalogue", file: "x_test.go",
src: "package catalogue\ntype Manifest struct{}\nfunc StreamProblems(m Manifest) []string { return nil }\n"},
} {
root := t.TempDir()
dir := filepath.Join(root, row.dir)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if row.file == "" {
row.file = "x.go"
}
if err := os.WriteFile(filepath.Join(dir, row.file), []byte(row.src), 0o644); err != nil {
t.Fatal(err)
}
got, err := Problems(root, nil)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(got, "\n")
if (row.refused == "") != (len(got) == 0) || !strings.Contains(joined, row.refused) {
t.Errorf("%s: refused %q, want %q", row.name, joined, row.refused)
}
}
dir, err := BesideDir()
if err != nil {
t.Fatal(err)
}
got, err := Problems(dir, composition)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("mesh-sdk holds a manifest judge:\n%s", strings.Join(got, "\n"))
}
}
+4
View File
@@ -0,0 +1,4 @@
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
# None: the tests read mesh-sdk's source, the clone beside this repository (MESH_CHECK_BESIDE), which is no path of it.
+37 -1
View File
@@ -4,7 +4,9 @@
// own code that is not a test (a tool's description, the notes and errors it answers with) and every
// description in its manifest. Comments are not read: an agent never sees them. And no daemon's line
// (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to
// type of the node-engine's or the controller's binary, where the operator's command is `nox`.
// type of the node-engine's or the controller's binary, where the operator's command is `nox`. And no address of the
// controller (novox/hq ADR 0590 §5): it is not placed, so a text naming one of its verbs, states or buckets sends its
// reader nowhere.
//
// A word is matched whole and in any case, a space in it matching any run of white space, and its plural
// (`s` or `es` on its last part) is matched as the word. A vendor word is
@@ -89,11 +91,45 @@ func run(root string) int {
len(findings))
return 1
}
// And no address of the controller (novox/hq ADR 0590 §5): it is not placed, so a text that sends an agent or the
// operator to one of its verbs, states or buckets sends them nowhere.
if findings := checkAddresses(texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d address(es) of the controller in what a module shows — name the seat that holds the job now (novox/hq ADR 0590)\n",
len(findings))
return 1
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files)
return 0
}
// controllerAddress is an address of the controller: its seat's verb or state (`mesh-controller.<name>`, with or without
// `seat:`) or one of its buckets (`mesh-controller_<bucket>`). The module's name in prose is no address.
var controllerAddress = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller[._][A-Za-z0-9<]`)
// checkAddresses is every text that names an address of the controller (novox/hq ADR 0590 §5).
func checkAddresses(texts []text) []string {
var out []string
for _, t := range texts {
if !strings.Contains(t.s, "mesh-controller") {
continue
}
if m := controllerAddress.FindStringIndex(t.s); m != nil {
line := t.line
if line > 0 {
line += strings.Count(t.s[:m[0]], "\n")
}
out = append(out, fmt.Sprintf("%s:%d: names an address of the controller, which is not placed — in %q", t.file, line,
excerpt(t.s, m[0], m[1])))
}
}
sort.Strings(out)
return out
}
// collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of
// its manifests.
func collect(root string) ([]text, int, error) {
+22
View File
@@ -95,3 +95,25 @@ func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
}
}
// ADR 0590 §5 (issue 1126 B10): no text a module shows names an address of the controller, which is not placed; prose
// naming it passes.
func TestCheckAddressesRefusesTheControllersAddresses(t *testing.T) {
for _, c := range []struct {
s string
want bool
}{
{"push this machine (mesh-controller.push)", true},
{"ask seat:mesh-controller.nodes", true},
{"the bucket mesh-controller_assignments", true},
{"`mesh-controller.<verb>`", true},
{"the mesh-controller module is gone.", false},
{"located in mesh-controller#246", false},
{"ask mesh-placement.placement", false},
} {
got := checkAddresses([]text{{file: "m/a.go", line: 1, s: c.s}})
if (len(got) == 1) != c.want {
t.Errorf("checkAddresses(%q) = %q, want a finding: %v", c.s, got, c.want)
}
}
}
+11 -2
View File
@@ -1,5 +1,14 @@
module adwaita
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.7
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -153,7 +153,7 @@ func TestOlderNamesNotReadAreSaidFirst(t *testing.T) {
func references(t *testing.T, a *asked) func(map[string]any) (any, error) {
f := world()
s := &Store{Container: "mesh-registry", Run: f.run(t)}
for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
for _, tl := range Tools(s, recordsAsked{a}, &Catalogue{Ask: a.ask}) {
if tl.Name == "artifact_store_references" {
return tl.Run
}
@@ -88,7 +88,7 @@ func (runtimeBus) Publish(subject string, body any) error {
return stdio.SeatPublish(subject, body, "")
}
// follow watches the builder's done keys and the wants: a build's end sweeps (once the lease names no controller) and
// follow watches the builder's done keys and the wants: a build's end sweeps and
// observes again; a want changed observes again. Event-driven: nothing reads them on a timer.
func follow(h *Holder) {
<-stdio.Served()
@@ -9,8 +9,8 @@ package main
// At each build's end (a done key written) it sweeps as the controller's take-in did: every kept archive held first
// (ADR 0251 §8), then what is eligible let go of, oldest first, within the same bound per sweep, an index left for a
// `collect` a person confirms (ADR 0251 §7), each deletion recorded in the hand-act log (seat mesh-hand-acts) and said
// as artifacts-gone. **Nothing is let go until the controller's lease names a holder that is no controller**: until
// then the controller's take-in sweeps, and two sweeps never run at once. It also observes S25, image-missing: a want's
// as artifacts-gone. **No lease turns it** (issue 1126): the controller ended by its unassignment, so its take-in sweeps
// nothing and this seat is the one sweep. It also observes S25, image-missing: a want's
// image the store does not hold, a finding in its own state `findings` the condition keeper reads.
import (
@@ -24,7 +24,6 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
@@ -40,7 +39,6 @@ const (
HavesRead = "node-engine.have"
// ManifestsRead is the indexer's manifests: the images a manifest pins by itself.
ManifestsRead = delivery.IndexerModule + ".manifests"
LeaseRead = buscompose.ControllerLease
// FindingsState is the holder's own findings (health.FindingsState): image-missing.
FindingsState = health.FindingsState
// Module is this module, the observer its findings name.
@@ -58,7 +56,7 @@ const (
ImageMissing = "image-missing"
)
// Bus is what the holder reads and writes on the bus: its read states (wants, builds, haves, the lease), its own
// Bus is what the holder reads and writes on the bus: its read states (wants, builds, haves), its own
// findings, and its seat's event. The runtime in main; a test's in memory.
type Bus interface {
Keys(state string) ([]string, error)
@@ -106,13 +104,6 @@ type Build struct {
Manifest string
}
// LeaseAway says the controller's lease names a holder that is no controller: then the seat sweeps.
func (h *Holder) LeaseAway() bool {
raw, found, err := h.Bus.Get(LeaseRead, buscompose.ControllerLeaseKey)
away, _ := buscompose.LeaseHandedAway(raw, found, err)
return away
}
// Builds is every done build the builder's keys hold, each with its end.
func (h *Holder) Builds() ([]Build, error) {
keys, err := h.Bus.Keys(BuildsRead)
@@ -495,10 +486,6 @@ func (h *Holder) Collect(why string, confirm bool, most int) (*CollectAnswer, er
if most > MostAtAll {
return nil, fmt.Errorf("collect lets go of at most %d at once, not %d", MostAtAll, most)
}
if confirm && !h.LeaseAway() {
return nil, errors.New("the controller holds its lease, and its take-in sweeps the store until it hands it away: " +
"this seat lets nothing go before then (novox/hq ADR 0571 clause 2); ask the controller's collect")
}
return h.Sweep(context.Background(), Sweeping{Confirmed: confirm, DryRun: !confirm, Most: most, Why: why, By: "collect"})
}
@@ -665,11 +652,8 @@ func (h *Holder) sayGone(j *judged, refs []string, how string) {
}
}
// AfterBuild is the sweep at a build's end: only once the lease names no controller.
// AfterBuild is the sweep at a build's end.
func (h *Holder) AfterBuild(key string) {
if !h.LeaseAway() {
return
}
a, err := h.Sweep(context.Background(), Sweeping{Most: MostPerSweep, By: "after the build " + key})
if err != nil {
h.logf("the sweep after %s: %v", key, err)
@@ -3,7 +3,7 @@
package main
// The seat mesh-artifact-sweep's holder whole against a real bus (novox/hq ADR 0571 clause 2, issue 1955): the builder's done
// keys, the wants and the controller's lease in a real nats-server's key-value buckets, a build's end heard by a watch
// keys and the wants in a real nats-server's key-value buckets, no lease key anywhere (issue 1126), a build's end heard by a watch
// of the builds as the runtime hands it over, each deletion recorded by a request to the seat mesh-hand-acts on the bus,
// artifacts-gone said on the seat's subject, and S25's finding written to the holder's own bucket. Only the store's
// door is in memory: its HTTP is the copied registry package's, tested there.
@@ -20,7 +20,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/testbus"
)
@@ -117,7 +116,7 @@ func (b realBus) Publish(subject string, body any) error {
return b.nc.Publish(subject, raw)
}
func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
func TestTheSeatSweepsAtABuildsEndOnARealBusWithNoLease(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
nc, err := nats.Connect(testbus.URL(t))
@@ -129,7 +128,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, s := range []string{BuildsRead, WantsRead, HavesRead, LeaseRead, ManifestsRead, "artifact-store-tools." + FindingsState} {
for _, s := range []string{BuildsRead, WantsRead, HavesRead, ManifestsRead, "artifact-store-tools." + FindingsState} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(s), History: 5}); err != nil {
t.Fatal(err)
}
@@ -137,9 +136,15 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
bus := realBus{ctx: ctx, nc: nc, js: js}
// The mesh as aMesh lays it, copied onto the real buckets: letta's seven builds, a want, the store's listing.
mem, memb, door, _ := aMesh(t, &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3})
bus.log = memb.log
for _, s := range []string{BuildsRead, WantsRead, LeaseRead, ManifestsRead} {
mem, memb, door, _ := aMesh(t)
// The builder's log as written today: aMesh dates it from 2026-10-01 for its fixed clock, and this holder runs on the
// real clock, so read as dated the entries fall out of the log's 30 days and every copy is kept as unrecorded.
shift := time.Since(memb.log[len(memb.log)-1].At)
for _, e := range memb.log {
e.At = e.At.Add(shift)
bus.log = append(bus.log, e)
}
for _, s := range []string{BuildsRead, WantsRead, ManifestsRead} {
keys, _ := memb.Keys(s)
for _, k := range keys[:min(len(keys), 6)] { // the seventh build ends later, on the bus
raw, _, _ := memb.Get(s, k)
@@ -169,7 +174,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
t.Fatal(err)
}
h := &Holder{Bus: findingsAt{realBus: bus}, Door: mem.Door, Listed: mem.Listed, Now: time.Now, Logf: t.Logf}
h.Acts = handacts.Recorder{LeaseAway: h.LeaseAway, Ask: func(key string, body any) (json.RawMessage, error) {
h.Acts = handacts.Recorder{Ask: func(key string, body any) (json.RawMessage, error) {
raw, _ := json.Marshal(body)
m, err := nc.Request("mesh.seat."+strings.TrimPrefix(strings.Replace(key, ".", ".tool.", 1), "seat:"), raw, 5*time.Second)
if err != nil {
@@ -198,18 +203,10 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
}
}
// While the lease names the controller, a build's end lets nothing go.
// No lease key: the build's end holds the kept archives and lets go of letta's 2nd, which the 7th moved past the five
// newest and no want names (the want keeps the 1st), recording each deletion.
ended()
if len(door.asked) != 0 {
t.Fatalf("before the lease write the store was asked %v", door.asked)
}
// The lease handed away: the next build's end holds the kept archives and lets go of letta's 2nd, which the 7th moved
// past the five newest and no want names (the want keeps the 1st), recording each deletion.
if err := bus.Put(LeaseRead, buscompose.ControllerLeaseKey, buscompose.LeaseHolder{HandedTo: "mesh-grants", Why: "the lease write"}); err != nil {
t.Fatal(err)
}
ended()
deadline := time.Now().Add(5 * time.Second)
deadline := time.Now().Add(30 * time.Second)
for {
mu.Lock()
n := len(gone)
@@ -11,7 +11,6 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health"
@@ -139,8 +138,8 @@ func archiveRef(module string, n int) string {
}
// A mesh with seven done builds of letta, the n-th ending n hours after a day began, each with an image and an archive;
// a want names the first on ace; the store lists them all and one image no build names; the lease as given.
func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memDoor, *[]string) {
// a want names the first on ace; the store lists them all and one image no build names; no lease key (issue 1126).
func aMesh(t *testing.T) (*Holder, *memBus, *memDoor, *[]string) {
t.Helper()
day := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
b := &memBus{states: map[string]map[string][]byte{}, history: map[string][]stdio.StateRevision{}}
@@ -194,9 +193,6 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD
_ = b.Put(ManifestsRead, mk, json.RawMessage(manifest))
want, _ := json.Marshal(delivery.Want{V: delivery.Version, FP: fp(1), Self: true})
_ = b.Put(WantsRead, "want.ace.letta", json.RawMessage(want))
if lease != nil {
_ = b.Put(LeaseRead, buscompose.ControllerLeaseKey, lease)
}
var acts []string
h := &Holder{Bus: b, Door: func() (Door, error) { return door, nil },
Listed: func(context.Context) (*View, error) { return &View{L: listing}, nil },
@@ -206,18 +202,14 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD
acts = append(acts, key+" "+string(raw))
return json.RawMessage(`{}`), nil
}}}
h.Acts.LeaseAway = h.LeaseAway
return h, b, door, &acts
}
// ADR 0571 clause 2: the keep set from the store's listing, the wants and the five newest builds; nothing let go of while
// the lease names a controller; once it names another holder, a build's end lets go of the eligible within the bound,
// leaves an index for a confirmed collect, records each deletion and says artifacts-gone.
func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t *testing.T) {
controller := &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3}
handed := &buscompose.LeaseHolder{HandedTo: "mesh-grants", HandedBy: "person.jochen", Why: "the lease write"}
h, _, _, _ := aMesh(t, controller)
// ADR 0571 clause 2, 1126/A4/AC2: the keep set from the store's listing, the wants and the five newest builds; with no
// lease key, a build's end and a confirmed collect let go of the eligible within the bound, leave an index for a
// confirmed collect, record each deletion as a hand act and say artifacts-gone.
func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsWithNoLease(t *testing.T) {
h, _, _, _ := aMesh(t)
r, err := h.Artifacts("")
if err != nil {
t.Fatal(err)
@@ -244,9 +236,10 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
t.Errorf("the references are %d, counted %v, at %q", len(r.References), r.Counts, r.Store)
}
all := []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}
for _, c := range []struct {
name string
lease *buscompose.LeaseHolder
how string // "build" (a build's end), "collect" (a confirmed collect), or a sweep asked with most
index bool
most int
letGo []string
@@ -254,45 +247,58 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
skipped int
unlog int
}{
{name: "the lease names a controller", lease: controller},
{name: "no lease key", lease: nil},
{name: "handed away", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}},
{name: "an index waits", lease: handed, index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1},
{name: "bounded", lease: handed, most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2},
{name: "a build's end", how: "build", letGo: all},
{name: "a confirmed collect", how: "collect", letGo: all},
{name: "a sweep", most: MostPerSweep, letGo: all},
{name: "an index waits", index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1},
{name: "bounded", most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2},
// A kept build the log no longer holds: what it stood on is unknown, so no copy goes.
{name: "a kept build's bases unknown", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3},
{name: "a kept build's bases unknown", most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3},
} {
t.Run(c.name, func(t *testing.T) {
h, b, door, acts := aMesh(t, c.lease)
h, b, door, acts := aMesh(t)
door.indexes[imageRef("letta", 2)] = c.index
if c.unlog > 0 {
b.log = append(b.log[:c.unlog-1:c.unlog-1], b.log[c.unlog:]...)
}
if c.most == 0 {
switch c.how {
case "build":
h.AfterBuild("letta.x")
if _, err := h.Collect("tidy", true, 0); err == nil {
t.Error("a confirmed collect was let through while the controller sweeps")
var gone []string
for _, x := range door.asked {
if r, ok := strings.CutPrefix(x, "let go "); ok {
gone = append(gone, r)
}
}
if len(door.asked) != 0 || len(*acts) != 0 || len(b.published) != 0 {
t.Fatalf("before the lease write the store was asked %v, acts %v, said %v", door.asked, *acts, b.published)
if fmt.Sprint(gone) != fmt.Sprint(c.letGo) {
t.Fatalf("a build's end let go of %v, want %v", gone, c.letGo)
}
default:
var a *CollectAnswer
var err error
if c.how == "collect" {
a, err = h.Collect("tidy", true, 0)
} else {
a, err = h.Sweep(context.Background(), Sweeping{Most: c.most, By: "after the build letta.x"})
}
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(a.LetGo) != fmt.Sprint(c.letGo) || a.Left != c.left || a.Skipped != c.skipped || a.Held != 6 || a.Missing != 0 {
t.Fatalf("let go of %v (left %d, skipped %d, held %d, missing %d), want %v", a.LetGo, a.Left, a.Skipped, a.Held, a.Missing, c.letGo)
}
return
}
a, err := h.Sweep(context.Background(), Sweeping{Most: c.most, By: "after the build letta.x"})
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(a.LetGo) != fmt.Sprint(c.letGo) || a.Left != c.left || a.Skipped != c.skipped || a.Held != 6 || a.Missing != 0 {
t.Fatalf("let go of %v (left %d, skipped %d, held %d, missing %d), want %v", a.LetGo, a.Left, a.Skipped, a.Held, a.Missing, c.letGo)
}
// Every kept archive held before the first deletion.
first := -1
for i, x := range door.asked {
if strings.HasPrefix(x, "let go ") && first < 0 {
first = i
held := 0
for _, x := range door.asked {
if strings.HasPrefix(x, "let go ") {
break
}
if strings.HasPrefix(x, "hold ") {
held++
}
}
if first != 6 {
if held != 6 {
t.Errorf("the store was asked %v: the six kept archives are held first", door.asked)
}
if len(*acts) != len(c.letGo) || !strings.HasPrefix((*acts)[0], "seat:mesh-hand-acts.hand-act ") ||
@@ -310,7 +316,7 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
// S25 (image-missing): a want whose build names an image the store does not hold is a finding in the seat's own state,
// written once and deleted once the store holds it again; images names what a machine's wants and haves name.
func TestAWantedImageTheStoreLacksIsAFindingAndImagesNamesWantsAndHaves(t *testing.T) {
h, b, _, _ := aMesh(t, nil)
h, b, _, _ := aMesh(t)
view, _ := h.Listed(context.Background())
repo, digest, _ := splitReference(imageRef("letta", 1))
delete(view.L.Repos[repo].Revisions, digest)
@@ -1,8 +1,7 @@
// The artifact-store-tools module's Go bundle (novox/hq ADR 0251 §1–3, ADR 0571 clause 2, to-be 51): a process the
// node's runtime launches and speaks MCP over stdio to. It says what the artifact store holds — its repositories, its
// size, and what the mesh keeps of each manifest — read from the store's own files through the store's own container,
// and it holds the seat mesh-artifact-sweep: the keep set, the sweep at each build's end once the controller's lease names
// no controller, and S25's image-missing.
// and it holds the seat mesh-artifact-sweep: the keep set, the sweep at each build's end, and S25's image-missing.
//
// **A module beside the store, not the store's own.** A module that provides the artifact store cannot also build an
// artifact: building publishes to the store, so the store would be needed to create itself. So these tools are a
@@ -29,7 +28,7 @@ func main() {
}
h := &Holder{Bus: runtimeBus{}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
Now: time.Now, Logf: logf}
h.Acts = handacts.Recorder{Ask: stdio.Ask, LeaseAway: h.LeaseAway}
h.Acts = handacts.Recorder{Ask: stdio.Ask}
go follow(h)
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): artifact-store-tools.
if err := stdio.Serve("", append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...)); err != nil {
@@ -2,22 +2,16 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
)
// What the records say, asked of the controller and never copied (novox/hq ADR 0251 §2). The
// controller decides what the mesh keeps; this bundle only sets that beside what the store holds.
// What the records say, in the shape the seat's holder answers them (Holder; novox/hq ADR 0251 §2, ADR 0571 clause 2):
// the holder decides what the mesh keeps; the tools only set that beside what the store holds.
// Asking is how a tool on the mesh is asked: stdio.Ask, or a test's.
type Asking func(key string, body any) (json.RawMessage, error)
// ControllerSeat is the seat whose verbs the controller serves.
const ControllerSeat = "mesh-controller"
// Record is one reference the mesh recorded making, as the controller's `artifacts` answers it.
// Record is one reference the mesh recorded making, as the seat's `artifacts` answers it.
type Record struct {
Reference string `json:"reference"`
Repository string `json:"repository,omitempty"`
@@ -27,7 +21,7 @@ type Record struct {
Why []string `json:"why,omitempty"`
}
// Records is the controller's `artifacts` answer.
// Records is the seat's `artifacts` answer.
type Records struct {
Store string `json:"store"`
KeptBuilds int `json:"kept_builds"`
@@ -39,7 +33,7 @@ type Records struct {
Note string `json:"-"`
}
// CollectAnswer is the controller's `collect` answer.
// CollectAnswer is the seat's `collect` answer.
type CollectAnswer struct {
DryRun bool `json:"dry_run"`
Store string `json:"store"`
@@ -53,15 +47,12 @@ type CollectAnswer struct {
Skipped int `json:"skipped"`
Left int `json:"left"`
Stopped string `json:"stopped"`
// Rest is whatever else the controller said, passed on as it said it.
// Rest is whatever else the sweep said, passed on as it said it.
Rest map[string]json.RawMessage `json:"-"`
}
// Controller asks the controller's seat through the runtime.
type Controller struct{ Ask Asking }
// answerOf reads a verb's answer whatever wraps it: the controller's {output, ok, answer}, a text the
// runtime handed over, or the protocol's content list.
// answerOf reads a verb's answer whatever wraps it: {output, ok, answer}, a text the runtime handed over, or the
// protocol's content list.
func answerOf(raw json.RawMessage) (json.RawMessage, string, bool) {
var s string
if json.Unmarshal(raw, &s) == nil {
@@ -95,89 +86,11 @@ func answerOf(raw json.RawMessage) (json.RawMessage, string, bool) {
return raw, string(raw), true
}
func (c Controller) verb(name string, args map[string]any) (json.RawMessage, error) {
if c.Ask == nil {
return nil, errors.New("this bundle has no way to ask the controller")
}
raw, err := c.Ask("seat:"+ControllerSeat+"."+name, args)
if err != nil {
return nil, fmt.Errorf("the controller's %s could not be asked: %w", name, err)
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, fmt.Errorf("the controller refused %s: %s", name, lastLine(output))
}
if len(answer) == 0 || string(answer) == "null" {
return nil, fmt.Errorf("the controller's %s answered no data: %s", name, lastLine(output))
}
return answer, nil
}
func lastLine(s string) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
return strings.TrimSpace(lines[len(lines)-1])
}
// Artifacts asks what the records say, with the references already let go of when the answer can carry
// them; when it cannot, without, and says so.
func (c Controller) Artifacts(repository string) (*Records, error) {
args := map[string]any{"collected": "true"}
if repository != "" {
args["repository"] = repository
}
raw, err := c.verb("artifacts", args)
collected := true
note := ""
if err != nil {
delete(args, "collected")
var again error
raw, again = c.verb("artifacts", args)
if again != nil {
return nil, again
}
collected = false
note = "the references the mesh already let go of could not be asked for (" + err.Error() +
"), so a manifest the mesh let go of that the store still holds is counted as unrecorded"
}
var r Records
if err := json.Unmarshal(raw, &r); err != nil {
return nil, fmt.Errorf("the controller's artifacts answer is not readable: %v", err)
}
for i := range r.References {
fill(&r.References[i])
}
r.CollectedAsked, r.Note = collected, note
return &r, nil
}
// Collect asks the controller's sweep: a dry run unless confirm, which needs why.
func (c Controller) Collect(why string, confirm bool, most int) (*CollectAnswer, error) {
args := map[string]any{}
if confirm {
if strings.TrimSpace(why) == "" {
return nil, errors.New("a real collection needs why: nothing was asked")
}
args["why"] = why
args["confirm"] = "true"
}
if most > 0 {
args["most"] = strconv.Itoa(most)
}
raw, err := c.verb("collect", args)
if err != nil {
return nil, err
}
var a CollectAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the controller's collect answer is not readable: %v", err)
}
_ = json.Unmarshal(raw, &a.Rest)
if !confirm && !a.DryRun && len(a.LetGo) > 0 {
return nil, fmt.Errorf("the controller let go of %d artifacts when only a dry run was asked", len(a.LetGo))
}
return &a, nil
}
// fill reads the repository, digest and kind out of a reference when the answer left them out.
func fill(r *Record) {
repo, digest, kind := splitReference(r.Reference)
@@ -9,6 +9,7 @@ import (
"os"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"testing"
@@ -280,7 +281,7 @@ func TestOnlyTheirBytesLeaveOutWhatAnotherStateMarks(t *testing.T) {
}
}
// asked records every ask, and answers as the controller would.
// asked records every ask, and answers as the seat and the catalogue would.
type asked struct {
mu sync.Mutex
calls []string
@@ -293,6 +294,51 @@ type asked struct {
noCatalogue bool
}
// recordsAsked is the tools' Recorded in a test: each call asked of asked, as `artifacts` or `collect`, with the
// arguments a real run gives the seat's sweep.
type recordsAsked struct{ a *asked }
func (r recordsAsked) Artifacts(repository string) (*Records, error) {
args := map[string]any{"collected": "true"}
if repository != "" {
args["repository"] = repository
}
raw, err := r.a.ask("artifacts", args)
if err != nil {
return nil, err
}
var recs Records
answer, _, _ := answerOf(raw)
if err := json.Unmarshal(answer, &recs); err != nil {
return nil, err
}
for i := range recs.References {
fill(&recs.References[i])
}
recs.CollectedAsked = true
return &recs, nil
}
func (r recordsAsked) Collect(why string, confirm bool, most int) (*CollectAnswer, error) {
args := map[string]any{}
if confirm {
if strings.TrimSpace(why) == "" {
return nil, fmt.Errorf("a real collection needs why: nothing was asked")
}
args["why"], args["confirm"] = why, "true"
}
if most > 0 {
args["most"] = strconv.Itoa(most)
}
raw, err := r.a.ask("collect", args)
if err != nil {
return nil, err
}
var c CollectAnswer
answer, _, _ := answerOf(raw)
return &c, json.Unmarshal(answer, &c)
}
func (a *asked) ask(key string, body any) (json.RawMessage, error) {
a.mu.Lock()
defer a.mu.Unlock()
@@ -304,7 +350,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
}
var answer any
switch key {
case "seat:mesh-controller.artifacts":
case "artifacts":
answer = records()
case CatalogueModules:
if a.noCatalogue {
@@ -331,7 +377,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
// The catalogue's tool answers through the runtime as the protocol's content list.
raw, _ := json.Marshal(map[string]any{"module": map[string]any{"module": name, "manifest": json.RawMessage(manifest)}})
return json.Marshal(map[string]any{"content": []map[string]any{{"type": "text", "text": string(raw)}}})
case "seat:mesh-controller.collect":
case "collect":
if args["confirm"] == "true" {
answer = CollectAnswer{LetGo: []string{"artifact-store://app/server@" + d("m-old")}}
} else {
@@ -347,7 +393,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
func tool(t *testing.T, a *asked, name string) func(map[string]any) (any, error) {
f := world()
s := &Store{Container: "mesh-registry", Run: f.run(t)}
for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
for _, tl := range Tools(s, recordsAsked{a}, &Catalogue{Ask: a.ask}) {
if tl.Name == name {
return tl.Run
}
@@ -390,21 +436,6 @@ func TestARealRunWithoutWhyIsRefusedAndAsksNothing(t *testing.T) {
}
}
func TestReferencesFallBackWhenCollectedCannotBeCarried(t *testing.T) {
a := &asked{fail: map[string]bool{"collected": true}}
out, err := tool(t, a, "artifact_store_references")(map[string]any{"repository": "app/server"})
if err != nil {
t.Fatal(err)
}
m := out.(map[string]any)
if m["note"] == nil || m["manifests"] == nil {
t.Errorf("answer %v", m)
}
if _, err := tool(t, a, "artifact_store_references")(map[string]any{"repository": "nope"}); err == nil {
t.Error("an unknown repository was answered")
}
}
func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
@@ -423,7 +454,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Fatal(err)
}
var served []string
for _, tl := range Tools(&Store{}, Controller{}, &Catalogue{}) {
for _, tl := range Tools(&Store{}, recordsAsked{&asked{}}, &Catalogue{}) {
if !strings.HasPrefix(tl.Name, "artifact_store_") || tl.Description == "" || tl.Run == nil {
t.Errorf("tool %q", tl.Name)
}
@@ -436,7 +467,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Fatalf("served %v, manifest %v", served, listed)
}
// ADR 0571 clause 2: it holds the seat mesh-artifact-sweep and serves its verbs, and calls no verb of the controller's;
// the lease is the one controller state it reads (clause 10's exemption).
// it reads no controller state, the lease neither (issue 1126).
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule}
if !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes %v", m.Invokes)
@@ -455,7 +486,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Errorf("claims %+v, serves %v, the seat's verbs %v", m.Claims, verbs, contract)
}
for _, r := range m.Reads {
if strings.HasPrefix(r, "mesh-controller.") && r != LeaseRead {
if strings.HasPrefix(r, "mesh-controller.") {
t.Errorf("reads the controller's %s", r)
}
}
@@ -12,7 +12,7 @@ import (
)
// Recorded is what says which references the mesh keeps and lets go of them: the seat's holder (Holder, novox/hq ADR
// 0571 clause 2), or before it the controller (Controller), each in the controller's answer's shape.
// 0571 clause 2), or a test's stand-in.
type Recorded interface {
Artifacts(repository string) (*Records, error)
Collect(why string, confirm bool, most int) (*CollectAnswer, error)
+1 -1
View File
@@ -3,7 +3,7 @@ module artifactstoretools
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+1 -2
View File
@@ -27,8 +27,7 @@
"mesh-delivery-builder.builds",
"mesh-delivery-builder.build-log",
"mesh-delivery-indexer.manifests",
"node-engine.have",
"mesh-controller.lease"
"node-engine.have"
],
"state": [
"findings"
@@ -171,7 +171,7 @@ func TestTheSwitcherDoesNotActOnAnotherModel(t *testing.T) {
go func() { sw.Run(context.Background()); close(done) }()
select {
case <-done:
case <-time.After(2 * time.Second):
case <-time.After(30 * time.Second):
t.Fatal("the switcher ran on another model")
}
if r := sw.Report(); r.Running || r.Disabled == "" || len(f.calls) != 0 {
+11 -2
View File
@@ -1,5 +1,14 @@
module asuszephyrusg14
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.6
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.6 h1:9qzdYONYbJdWcu6sxQcq9v1LI0JxcfkiKYkMUzJSkVQ=
git.novox.be/novox/mesh-sdk/go v0.1.6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+11 -2
View File
@@ -1,5 +1,14 @@
module avahi
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.6
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.6 h1:9qzdYONYbJdWcu6sxQcq9v1LI0JxcfkiKYkMUzJSkVQ=
git.novox.be/novox/mesh-sdk/go v0.1.6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+11 -2
View File
@@ -1,5 +1,14 @@
module blueman
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.7
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+11 -2
View File
@@ -1,5 +1,14 @@
module bluetooth
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.6
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.6 h1:9qzdYONYbJdWcu6sxQcq9v1LI0JxcfkiKYkMUzJSkVQ=
git.novox.be/novox/mesh-sdk/go v0.1.6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+1 -1
View File
@@ -181,7 +181,7 @@ func standInCapture(release <-chan struct{}, fail error) func(ctx context.Contex
// waitFor polls until the condition holds.
func waitFor(t *testing.T, what string, cond func() bool) {
t.Helper()
for i := 0; i < 400; i++ {
for i := 0; i < 3000; i++ {
if cond() {
return
}
+1 -1
View File
@@ -181,7 +181,7 @@ func TestAFinishedGlassDocumentIsNotFiledAgainByItsExpiry(t *testing.T) {
if _, err := r.call(t, "scanner.scan", docArgs(id, true)); err != nil {
t.Fatal(err)
}
time.Sleep(400 * time.Millisecond)
waitFor(t, "the expired document to be closed and filed", func() bool { e, _ := os.ReadDir(folder); return len(e) >= 1 })
if e, _ := os.ReadDir(folder); len(e) != 1 {
t.Errorf("%d files, want 1", len(e))
}
@@ -311,8 +311,13 @@ func TestASlowUploadAndPrintAnswersStartedNeverATimeout(t *testing.T) {
}
got, _ = r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "g14"})
id2, _ := got.(map[string]any)["ask"].(string)
time.Sleep(300 * time.Millisecond)
if _, err := r.call(t, "printer.print", map[string]any{"ask": id2}); err == nil || !strings.Contains(err.Error(), "refused by name") {
t.Errorf("the failed upload's outcome: %v", err)
var outcome error
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(10 * time.Millisecond) {
if _, outcome = r.call(t, "printer.print", map[string]any{"ask": id2}); outcome != nil {
break
}
}
if outcome == nil || !strings.Contains(outcome.Error(), "refused by name") {
t.Errorf("the failed upload's outcome: %v", outcome)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module brother
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -4
View File
@@ -1,7 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020060201-88581d0acaef h1:xy3wuGWJpqMAXYdXolocYG6w9xnWqBltVUffoaziIKo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020060201-88581d0acaef/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f h1:7J/9jOfSfjfYpf8MOj96HwQRViQUkBNMb71RHP2mjn8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+4 -2
View File
@@ -120,8 +120,10 @@ nothing else of the home. A project is one entry of the projects directory and a
The agent kept reaching for `ssh <machine> journalctl` while the service manager's `journal` verb existed. So:
- **The "instead of" table.** Every seat verb and module tool may say which shell commands it replaces
(`replaces`, in the seat's definition or the module's manifest). The module asks the controller — `tools`,
`modules`, `nodes`, `node` — at start, every ten minutes and on `claude_code_render`, keeps the answer in its
(`replaces`, in the seat's definition or the module's manifest). The module reads the bus — the seats' verbs from the
seats' contract, the placed modules' manifests from `mesh-delivery-indexer.modules` and `.manifests`, the
machines and where each module runs from `mesh-delivery-assigner.placement` (issue 1126: the controller that
answered this is not placed) — at start, every ten minutes and on `claude_code_render`, keeps it in its
state (`mesh-tools.json`), and renders it into `CLAUDE.md` as one row per seat or module: `<node>/<seat>.` and
each verb with the commands it replaces. Generated, never written by hand: a verb that gains `replaces` is in
the next render. Ordered by what the guard here refused most, then the machines' seats, the modules, the
@@ -505,7 +505,7 @@ func TestTheHelperReadsOnlyARegularFile(t *testing.T) {
if a.Err == "" || len(a.Data) != 0 {
t.Fatalf("%s was read: %q", path, a.Data)
}
case <-time.After(5 * time.Second):
case <-time.After(30 * time.Second):
t.Fatalf("reading %s held the helper", path)
}
}
@@ -567,7 +567,7 @@ func TestAHelperThatDoesNotAnswerFailsTheRequestInTime(t *testing.T) {
if err == nil || !errors.Is(err, errHelperSlow) {
t.Fatalf("a silent helper answered: %v", err)
}
if time.Since(start) > 2*time.Second {
if time.Since(start) > 10*time.Second {
t.Fatalf("the request waited %s", time.Since(start))
}
}
@@ -7,6 +7,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
@@ -260,16 +261,25 @@ func TestAHomeItemIsImportedAndTheStaleOnesAreNamed(t *testing.T) {
}
}
// The manifest lists exactly the tools the bundle serves: a tool missing from it is never announced.
// The manifest lists exactly the tools the bundle serves: a tool missing from it is never announced. It invokes the
// hand-act seat's warranted and no verb of the controller, and the warrant is asked of that seat (ADR 0565 clause 11).
func TestTheManifestListsEveryToolServed(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Tools []string `json:"tools"`
Tools []string `json:"tools"`
Invokes []string `json:"invokes"`
}
_ = json.Unmarshal(raw, &m)
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted"}) {
t.Errorf("invokes = %v, want the hand-act seat's warranted alone", m.Invokes)
}
if src, _ := os.ReadFile("proposals.go"); !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) ||
strings.Contains(string(src), "seat:mesh-controller.warranted") {
t.Error("recordWarrant does not ask the hand-act seat alone")
}
listed := map[string]bool{}
for _, n := range m.Tools {
listed[n] = true
@@ -8,22 +8,26 @@ import (
"strconv"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// meshForTheGuard is a mesh of two machines and the verbs that replace what an agent runs over ssh, as the
// controller's records say them.
// seats' contract and the modules' manifests say them.
func meshForTheGuard() GuardData {
tools := json.RawMessage(`{"seats":[
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do.","replaces":["hostnamectl","uptime"]}]},
{"seat":"node-service-manager","scope":"node","tools":[
{"name":"status","description":"One unit as the service manager sees it now: its states.","replaces":["systemctl status","systemctl is-active"]},
{"name":"restart","description":"Restart one unit.","replaces":["systemctl restart"]},
{"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]},
{"seat":"node-hostname","scope":"node","tools":[
{"name":"add","description":"Add one address and its names to the operator's lines of /etc/hosts.","replaces":["edit /etc/hosts","HOSTALIASES"]}]}]}`)
modules := json.RawMessage(`[{"module":"docker","on":["anchor"],"replaces":{"docker_logs":["docker logs"],"docker_list":["docker ps"]}},
{"module":"systemd","on":["anchor","laptop"]}]`)
return GuardData{Node: "laptop", Replacements: ReplacementsOf(tools, modules), Machines: []Machine{
seats := []catalogue.Seat{
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
{Name: "status", Description: "One unit as the service manager sees it now: its states.", Replaces: []string{"systemctl status", "systemctl is-active"}},
{Name: "restart", Description: "Restart one unit.", Replaces: []string{"systemctl restart"}},
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
{Name: "node-hostname", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
{Name: "add", Description: "Add one address and its names to the operator's lines of /etc/hosts.", Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}}},
}
modules := []ModuleTools{{Module: "docker", On: []string{"anchor"}, Replaces: map[string][]string{"docker_logs": {"docker logs"}, "docker_list": {"docker ps"}}},
{Module: "systemd", On: []string{"anchor", "laptop"}}}
return GuardData{Node: "laptop", Replacements: ReplacementsOf(seats, modules), Machines: []Machine{
{Name: "anchor", Domains: []string{"anchor" + InternalSuffix, "anchor.example"}, Addresses: []string{"192.0.2.10", "10.10.0.1"}},
{Name: "laptop", Domains: []string{"laptop" + InternalSuffix}, Addresses: []string{"10.10.0.4"}},
}}
@@ -69,21 +73,21 @@ func bash(command string) HookInput {
func TestSSHToAMeshMachineIsRefusedNamingTheTool(t *testing.T) {
g := guard()
for command, want := range map[string]string{
"ssh anchor journalctl -u mesh-controller -n 50": "anchor/node-service-manager.journal",
"ssh anchor journalctl -u mesh-config -n 50": "anchor/node-service-manager.journal",
"ssh anchor 'sudo journalctl -fu sshd'": "anchor/node-service-manager.journal",
"ssh -p 22 root@anchor.example systemctl status nats": "anchor/node-service-manager.status",
"ssh anchor.internal systemctl restart nats": "anchor/node-service-manager.restart",
"ssh -o StrictHostKeyChecking=no 10.10.0.1 docker ps -a": "anchor/docker.docker_list",
"ssh anchor docker logs --tail 100 nats": "anchor/docker.docker_logs",
"ssh laptop docker logs x": "anchor/docker.docker_logs",
"ssh a uptime": "mesh-controller.node",
"ssh a uptime": "mesh-placement.placement",
"cd /tmp && ssh anchor journalctl": "anchor/node-service-manager.journal",
"sudo -u root ssh anchor journalctl": "anchor/node-service-manager.journal",
"timeout 10 ssh anchor journalctl": "anchor/node-service-manager.journal",
"env LANG=C ssh anchor journalctl": "anchor/node-service-manager.journal",
"bash -c 'ssh anchor journalctl -u x'": "anchor/node-service-manager.journal",
`echo "$(ssh anchor journalctl -n 5)" | tail`: "anchor/node-service-manager.journal",
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-controller.node",
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-placement.placement",
"ssh -J anchor 203.0.113.9 journalctl": "anchor/node-service-manager.journal",
"ssh via-anchor journalctl": "anchor/node-service-manager.journal",
"ssh nas.lan journalctl": "anchor/node-service-manager.journal",
@@ -1,43 +1,82 @@
package main
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// askingAController answers the controller's verbs as the seat does: `tools` in-process, the rest as the
// command's output and its JSON.
func askingAController(t *testing.T) Ask {
// aBus is the states claude-code reads, as a test holds them: by read, key and value.
type aBus map[string]map[string][]byte
func (b aBus) Keys(read string) ([]string, error) {
if b[read] == nil {
return nil, errors.New("no such state")
}
var keys []string
for k := range b[read] {
keys = append(keys, k)
}
return keys, nil
}
func (b aBus) Get(read, key string) ([]byte, bool, error) {
v, ok := b[read][key]
return v, ok, nil
}
// someSeats are two seats as the seats' contract says them: one held once for the mesh, one on every machine.
func someSeats() []catalogue.Seat {
return []catalogue.Seat{
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}},
{Name: "propose", Description: "Propose a change."}}},
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot.", Replaces: []string{"systemctl status", "systemctl is-active"}},
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
}
}
// aMesh is two machines, anchor (with a public domain) and laptop, docker and zsh placed on anchor, docker's manifest
// saying what its tool replaces, as the seat mesh-placement and the indexer keep them on the bus.
func aMesh(t *testing.T) aBus {
t.Helper()
wrap := func(output string, answer any) json.RawMessage {
raw, _ := json.Marshal(map[string]any{"ok": true, "output": output, "answer": answer})
return raw
}
return func(address string, args any) (json.RawMessage, error) {
switch address {
case "seat:mesh-controller.tools":
return json.RawMessage(`{"seats":[
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do, what it is assigned, and why.","replaces":["hostnamectl","uptime"]},{"name":"nodes","description":"Every machine."}]},
{"seat":"node-service-manager","scope":"node","tools":[
{"name":"status","description":"One unit as the service manager sees it now: its states, whether it starts at boot.","replaces":["systemctl status","systemctl is-active"]},
{"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]}]}`), nil
case "seat:mesh-controller.modules":
return wrap("[...]", []any{map[string]any{"module": "docker", "on": []string{"anchor"},
"replaces": map[string][]string{"docker_logs": {"docker logs"}}}, map[string]any{"module": "zsh", "on": []string{"anchor"}}}), nil
case "seat:mesh-controller.nodes":
return wrap("", []any{map[string]any{"name": "anchor"}, map[string]any{"name": "laptop"}}), nil
case "seat:mesh-controller.node":
node, _ := args.(map[string]any)["node"].(string)
if node == "anchor" {
return wrap("anchor\n mode converged\n public domain Anchor.Example\n\n what it runs\n", nil), nil
}
return wrap("laptop\n mode converged\n", nil), nil
b := aBus{PlacementRead: {}, ModulesRead: {}, ManifestsRead: {}}
for _, p := range []delivery.Placement{
{V: 1, Node: "anchor", Decided: delivery.NodeDecided{PublicDomain: "Anchor.Example"},
Modules: []delivery.PlacedModule{{Module: "docker"}, {Module: "zsh"}}},
{V: 1, Node: "laptop"},
} {
values, err := delivery.PlacementValues(p)
if err != nil {
t.Fatal(err)
}
for k, v := range values {
b[PlacementRead][k] = v
}
return nil, errors.New("no such verb")
}
for module, manifest := range map[string]string{
"docker": `{"module":"docker","replaces":{"docker_logs":["docker logs"]}}`,
"zsh": `{"module":"zsh"}`,
} {
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(manifest)))
mkey, _ := delivery.ManifestsKey(digest)
raw, err := json.Marshal(delivery.Manifest{V: 1, Module: module, JSON: manifest})
if err != nil {
t.Fatal(err)
}
b[ManifestsRead][mkey] = raw
key, _ := delivery.ModulesKey(module)
b[ModulesRead][key], _ = json.Marshal(delivery.Module{V: 1, None: "nothing is built of it", Manifest: digest, Repository: "novox/mesh-catalog", Commit: "c"})
}
return b
}
func resolving(name string) []string {
@@ -45,10 +84,11 @@ func resolving(name string) []string {
"laptop": {"127.0.0.1", "10.10.0.4"}}[name]
}
// What the controller says of its tools and machines is read whole: the seats' verbs, the modules' own tools,
// every machine with its domains and addresses — a loopback address never one of them.
func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
m, err := AskMeshTools(askingAController(t), resolving)
// Issue 1126 B13: what the mesh says of its tools and machines is read from the bus whole, never asked of the
// controller: the seats' verbs, the placed modules' own tools, every machine with its domains and addresses — a
// loopback address never one of them.
func TestTheMeshsToolsAreReadFromTheBus(t *testing.T) {
m, err := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
if err != nil {
t.Fatal(err)
}
@@ -56,7 +96,7 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
for _, r := range m.Replacements {
addresses = append(addresses, r.Address)
}
if got := strings.Join(addresses, ","); got != "mesh-controller.node,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
if got := strings.Join(addresses, ","); got != "mesh-placement.placement,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
t.Errorf("replacements: %s", got)
}
if len(m.Machines) != 2 || strings.Join(m.Machines[0].Domains, ",") != "anchor.internal,anchor.example" ||
@@ -71,12 +111,12 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
// **The table is generated, not written** (novox/hq ADR 0245): what the records say a verb replaces is a row,
// the machines' seats first, and nothing is rendered where the records say nothing.
func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
m, _ := AskMeshTools(askingAController(t), resolving)
m, _ := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
table := InsteadOf(&m)
for _, want := range []string{
"| `<node>/node-service-manager.` | `status` — `systemctl status`, `systemctl is-active` · `journal` — `journalctl` |",
"| `<node>/docker.` | `docker_logs` — `docker logs` |",
"| `mesh-controller.` | `node` — `hostnamectl`, `uptime` |",
"| `mesh-placement.` | `placement` — `hostnamectl`, `uptime` |",
"Never `ssh` to a mesh machine",
} {
if !strings.Contains(table, want) {
@@ -84,14 +124,14 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
}
}
if strings.Index(table, "node-service-manager.") > strings.Index(table, "docker.") ||
strings.Index(table, "docker.") > strings.Index(table, "`mesh-controller.`") {
strings.Index(table, "docker.") > strings.Index(table, "`mesh-placement.`") {
t.Errorf("not the machines' seats, then modules, then the mesh's:\n%s", table)
}
// What the guard here named most comes first: its row, and in its row its verb.
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-controller.node": 1}
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-placement.placement": 1}
ranked := InsteadOf(&m)
if !strings.Contains(ranked, "| `<node>/node-service-manager.` | `journal` — `journalctl` · `status`") ||
strings.Index(ranked, "`mesh-controller.`") > strings.Index(ranked, "`<node>/docker.`") {
strings.Index(ranked, "`mesh-placement.`") > strings.Index(ranked, "`<node>/docker.`") {
t.Errorf("not ranked by the guard's record:\n%s", ranked)
}
m.Refused = nil
@@ -121,7 +161,7 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
// Asked again with the same answer, nothing is written and nothing renders.
func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
p := Paths{State: t.TempDir()}
m, _ := AskMeshTools(askingAController(t), resolving)
m, _ := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
if !KeepMeshTools(p, m) {
t.Fatal("the first answer was not kept")
}
@@ -138,18 +178,32 @@ func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
}
}
// Where claude-code runs is read from the controller's `modules` answer as it comes — JSON, from `module list
// --json` — and from the printed list too.
func TestTheMachinesRunningTheModuleAreReadFromTheControllersAnswer(t *testing.T) {
asJSON := json.RawMessage(`{"ok":true,"output":"[...]","answer":[{"module":"zsh","on":["a"]},{"module":"claude-code","on":["a","b"]}]}`)
if got := strings.Join(NodesRunning(asJSON, "claude-code"), ","); got != "a,b" {
t.Errorf("from JSON: %q", got)
// Where a module runs is read from the seat mesh-placement's placement (issue 1126 B13), never the controller's answer.
func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
b := aMesh(t)
if got, err := NodesRunning(b, "zsh"); err != nil || strings.Join(got, ",") != "anchor" {
t.Errorf("zsh runs on %v (%v)", got, err)
}
printed := json.RawMessage(`{"ok":true,"output":"zsh 1 built x on a\nclaude-code 1 built y on a, b\n"}`)
if got := strings.Join(NodesRunning(printed, "claude-code"), ","); got != "a,b" {
t.Errorf("from the printed list: %q", got)
if got, err := NodesRunning(b, "claude-code"); err != nil || got != nil {
t.Errorf("a module that runs nowhere: %v (%v)", got, err)
}
if got := NodesRunning(json.RawMessage(`{"ok":true,"answer":[{"module":"zsh","on":["a"]}]}`), "claude-code"); got != nil {
t.Errorf("a module that runs nowhere: %v", got)
if _, err := NodesRunning(aBus{}, "zsh"); err == nil {
t.Error("an unreadable placement was read as no machine")
}
}
// Issue 1126 B13 AC1 (ADR 0590): over the seats the mesh defines, the rendered instructions, their table among them,
// name no address of the controller, which is not placed: each job is named by the seat that owns it now.
func TestTheRenderedInstructionsNameNoControllerAddress(t *testing.T) {
m, err := ReadMeshToolsFrom(aMesh(t), catalogue.Seats(), resolving)
if err != nil {
t.Fatal(err)
}
md := RenderWithMesh(Facts{Node: "w", Console: "x"}, Settings{}, nil, "/h", nil, Config{}, &m)["CLAUDE.md"]
if !strings.Contains(md, "## Instead of a shell command") {
t.Fatalf("no table rendered:\n%s", md)
}
if strings.Contains(md, "mesh-controller") {
t.Errorf("the instructions name the controller:\n%s", md)
}
}
@@ -2,11 +2,12 @@ package main
// instructionsText is the managed instruction file, generated from the TypeScript renderer it replaced so
// the file under the agent's managed directory did not change by a byte when the module moved to Go;
// a test holds it to that renderer's own output (testdata/rendered-by-typescript.json).
// a test holds it to that renderer's output (testdata/rendered-by-typescript.json), changed since only
// where the controller's verbs were named (issue 1126 B13: the controller is not placed, ADR 0590).
func instructionsText(node, role string) string {
return "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `" +
node +
"`\n- **Role:** " +
role +
"\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
"\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
}
@@ -133,7 +133,7 @@ func TestAJudgementWaitsItsTurnWithinTheBound(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "another judgement") {
t.Fatalf("%v", err)
}
if time.Since(start) > 2*time.Second {
if time.Since(start) > 10*time.Second {
t.Fatal("waited past the bound")
}
}
@@ -175,7 +175,7 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
j := testJudge(true, true, func(ctx context.Context, _ []string, _ []byte) ([]byte, error) {
deadline, _ = ctx.Deadline()
select {
case <-time.After(400 * time.Millisecond):
case <-time.After(1500 * time.Millisecond): // well past Wait, however loaded the machine
return []byte(`{"structured_output":{"verdict":"none"}}`), nil
case <-ctx.Done():
return nil, ctx.Err()
@@ -191,7 +191,7 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
t.Fatalf("not a ticket: %+v", out)
}
var got *Judged
for i := 0; i < 100; i++ {
for i := 0; i < 600; i++ {
got, err = j.Collect(out.Ticket)
if err != nil {
t.Fatal(err)
@@ -223,12 +223,12 @@ func TestALongJudgementIsBoundedAndSaysWhyItFailed(t *testing.T) {
t.Fatalf("a bound past the most taken: %v", err)
}
j.Wait = 20 * time.Millisecond
j.Bound = 30 * time.Millisecond // the test's stand-in for JudgeBound: within is past it, so a ticket
out, err := j.Judge(JudgeAsk{Question: "q", Schema: verdictSchema, Within: 200 * time.Millisecond})
j.Bound = time.Second // the test's stand-in for JudgeBound: within is past it, so a ticket; far past Wait on a loaded machine
out, err := j.Judge(JudgeAsk{Question: "q", Schema: verdictSchema, Within: 2 * time.Second})
if err != nil || !out.Pending {
t.Fatalf("%+v %v", out, err)
}
for i := 0; i < 100; i++ {
for i := 0; i < 3000; i++ {
got, err := j.Collect(out.Ticket)
if err != nil {
if !strings.Contains(err.Error(), "did not answer within") {
+16 -48
View File
@@ -23,6 +23,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
func say(format string, args ...any) {
@@ -204,54 +205,21 @@ func (s stateOf) Get(key string) (json.RawMessage, bool, error) {
return e.Value, true, nil
}
// nodesRunningMe is the nodes claude-code runs on, from the controller's list of modules — for the register
// nodesRunningMe is the nodes claude-code runs on, from the seat mesh-placement's placement — for the register
// tool's question.
func nodesRunningMe() ([]string, error) {
raw, err := ask("seat:mesh-controller.modules", map[string]any{})
if err != nil {
return nil, err
}
return NodesRunning(raw, "claude-code"), nil
}
func nodesRunningMe() ([]string, error) { return NodesRunning(busStates{}, "claude-code") }
// NodesRunning reads where a module runs from the controller's `modules` answer. The verb runs `module list
// --json`, so the answer is a JSON list of {module, on}: the lines of the printed list it was once read as
// never came, and every "also on" hint and every `nodes: "all"` named no machine. The printed form, `<module>
// … on a, b`, is still read when that is what came.
func NodesRunning(raw json.RawMessage, module string) []string {
var listed []struct {
Module string `json:"module"`
On []string `json:"on"`
// busStates reads the states the manifest declares in its reads through the runtime.
type busStates struct{}
func (busStates) Keys(read string) ([]string, error) { return stdio.State(read).Keys() }
func (busStates) Get(read, key string) ([]byte, bool, error) {
e, err := stdio.State(read).Get(key)
if err != nil || e == nil {
return nil, false, err
}
if json.Unmarshal(verbAnswer(raw), &listed) == nil {
for _, m := range listed {
if m.Module == module {
return m.On
}
}
return nil
}
text := outputOf(raw)
if text == "" {
text = string(raw)
}
for _, line := range strings.Split(text, "\n") {
if !strings.HasPrefix(line, module+" ") {
continue
}
_, on, ok := strings.Cut(line, " on ")
if !ok || strings.TrimSpace(on) == "nothing" {
return nil
}
var out []string
for _, n := range strings.Split(on, ",") {
if n = strings.TrimSpace(n); n != "" {
out = append(out, n)
}
}
return out
}
return nil
return e.Value, true, nil
}
func fingerprintOfFile(path string) any {
@@ -320,11 +288,11 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
Description: "Claude Code on this machine as the mesh configured it: the licence it holds and when its token expires, what it reports holding, the managed files, the MCP servers registered here. Fingerprints only, never a token.",
Run: func(map[string]any) (any, error) { return status(p), nil }},
{Name: "claude_code_render",
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after asking the controller again what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after reading the bus again for what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
Run: func(map[string]any) (any, error) {
answer := map[string]any{}
if m, err := AskMeshTools(ask, lookup); err != nil {
answer["asked"] = "the controller did not answer in full (" + err.Error() + "); what was kept is rendered"
if m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup); err != nil {
answer["asked"] = "the bus could not be read in full (" + err.Error() + "); what was kept is rendered"
} else {
KeepMeshTools(p, m)
}
@@ -950,11 +950,11 @@ func TestTheWatchHandlerReturnsAtOnceAndDrainAppliesWhatItNoted(t *testing.T) {
if err != nil {
t.Fatal(err)
}
case <-time.After(2 * time.Second):
case <-time.After(30 * time.Second):
t.Fatalf("the watch handler waited for the look or the state (%d of 400 returned)", i)
}
}
if took := time.Since(start); took > time.Second {
if took := time.Since(start); took > 30*time.Second {
t.Fatalf("400 values took %s to hand over", took)
}
shanks.mu.Unlock()
+160 -107
View File
@@ -4,21 +4,27 @@ package main
// shell need it (novox/hq ADR 0245): every seat verb and module tool that says what shell command it
// replaces, and the mesh's machines by every name and address a session could reach one by.
//
// Asked of the controller — `tools` for the seats' verbs, `modules` for the modules' own tools, `nodes`
// and `node` for the machines — at start and every few minutes, kept in the module's state so a render
// never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb that
// gains a `replaces` in the records is in the next render's table and the guard's next refusal.
// Read from the bus at start and every few minutes (issue 1126 B13; the controller that answered this before is
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-sdk catalogue), the modules'
// own tools from the manifests the indexer keeps (`mesh-delivery-indexer.modules` and `.manifests`), and the
// machines and where each module runs from the seat mesh-placement's `placement`. Kept in the module's state so
// a render never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb
// that gains a `replaces` is in the next render's table and the guard's next refusal.
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Replacement is one verb or tool and the shell commands it is the mesh's way to do.
@@ -41,11 +47,11 @@ type Machine struct {
Addresses []string `json:"addresses,omitempty"`
}
// MeshTools is what the controller said, as kept in the module's state.
// MeshTools is what the bus said, as kept in the module's state.
type MeshTools struct {
Replacements []Replacement `json:"replacements"`
Machines []Machine `json:"machines"`
// Asked is when the controller last answered.
// Asked is when the bus was last read whole.
Asked string `json:"asked,omitempty"`
// Refused is how often the guard here named each tool instead of a work-around, from its record: the
// table's order. Never kept — read from the record at each render.
@@ -78,42 +84,120 @@ func ReadMeshTools(p Paths) *MeshTools {
return &m
}
// verbAnswer is a controller verb's answer: the command's printed output, and its JSON when it printed one.
func verbAnswer(raw json.RawMessage) json.RawMessage {
var r struct {
Output string `json:"output"`
Answer json.RawMessage `json:"answer"`
// The states read, each declared in the manifest's reads.
const (
PlacementRead = delivery.PlacementRead
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
)
// States reads the module states the manifest declares in its reads: a state's keys, and one key's value.
type States interface {
Keys(read string) ([]string, error)
Get(read, key string) ([]byte, bool, error)
}
// ModuleTools is one module's own tools that replace a command, as its manifest says (`replaces`), and where it runs.
type ModuleTools struct {
Module string
On []string
Replaces map[string][]string
}
// placements is every node's placement, from the seat mesh-placement's state.
func placements(st States) (map[string]delivery.Placement, error) {
keys, err := st.Keys(PlacementRead)
if err != nil {
return nil, fmt.Errorf("%s: %w", PlacementRead, err)
}
if json.Unmarshal(raw, &r) != nil {
return raw
}
if len(r.Answer) > 0 && string(r.Answer) != "null" {
return r.Answer
}
if r.Output != "" {
t := strings.TrimSpace(r.Output)
for _, open := range []string{"[", "{"} {
if strings.HasPrefix(t, open) {
return json.RawMessage(t)
}
if i := strings.Index(t, "\n"+open); i >= 0 {
return json.RawMessage(strings.TrimSpace(t[i+1:]))
}
read := delivery.NewPlacementKeys()
for _, k := range keys {
v, found, err := st.Get(PlacementRead, k)
if err != nil {
return nil, fmt.Errorf("%s %s: %w", PlacementRead, k, err)
}
if found {
_, _ = read.Put(k, v, 0, false) // a key that does not read places nothing
}
return json.RawMessage(nil)
}
return raw
return read.All(), nil
}
func outputOf(raw json.RawMessage) string {
var r struct {
Output string `json:"output"`
// runsOn is, by module, the nodes placement puts it on, sorted.
func runsOn(placed map[string]delivery.Placement) map[string][]string {
out := map[string][]string{}
for node, p := range placed {
for _, m := range p.Modules {
out[m.Module] = append(out[m.Module], node)
}
}
_ = json.Unmarshal(raw, &r)
return r.Output
for _, nodes := range out {
sort.Strings(nodes)
}
return out
}
var publicDomain = regexp.MustCompile(`(?m)^\s*public domain\s+(\S+)\s*$`)
// NodesRunning is the nodes placement puts module on.
func NodesRunning(st States, module string) ([]string, error) {
placed, err := placements(st)
if err != nil {
return nil, err
}
return runsOn(placed)[module], nil
}
// modulesTools is every placed module's own tools that replace a command, by module, from the manifest the indexer
// keeps of it. A module whose manifest cannot be read is left out and said in the error.
func modulesTools(st States, on map[string][]string) ([]ModuleTools, error) {
var out []ModuleTools
var problems []error
modules := make([]string, 0, len(on))
for m := range on {
modules = append(modules, m)
}
sort.Strings(modules)
for _, module := range modules {
key, err := delivery.ModulesKey(module)
if err != nil {
continue
}
raw, found, err := st.Get(ModulesRead, key)
if err != nil || !found {
if err != nil {
problems = append(problems, fmt.Errorf("%s %s: %w", ModulesRead, module, err))
}
continue
}
m, err := delivery.DecodeModule(raw)
if err != nil || m.Manifest == "" {
continue
}
mkey, err := delivery.ManifestsKey(m.Manifest)
if err != nil {
continue
}
raw, found, err = st.Get(ManifestsRead, mkey)
if err != nil || !found {
if err != nil {
problems = append(problems, fmt.Errorf("%s of %s: %w", ManifestsRead, module, err))
}
continue
}
kept, err := delivery.DecodeManifest(raw)
if err != nil {
problems = append(problems, fmt.Errorf("the manifest of %s: %w", module, err))
continue
}
var man struct {
Replaces map[string][]string `json:"replaces"`
}
if json.Unmarshal([]byte(kept.JSON), &man) != nil || len(man.Replaces) == 0 {
continue
}
out = append(out, ModuleTools{Module: module, On: on[module], Replaces: man.Replaces})
}
return out, errors.Join(problems...)
}
// firstSentence is what a verb does, short: its description to the first full stop, at most 70 characters.
func firstSentence(s string) string {
@@ -131,52 +215,33 @@ func firstSentence(s string) string {
return s
}
// ReplacementsOf reads the controller's `tools` and `modules` answers: the seats' verbs first, in the
// records' order, then the modules' own tools by module and tool.
func ReplacementsOf(tools, modules json.RawMessage) []Replacement {
// ReplacementsOf is every seat verb and module tool that says it replaces a command: the seats' verbs first, in the
// seats' order, then the modules' own tools by module and tool.
func ReplacementsOf(seats []catalogue.Seat, modules []ModuleTools) []Replacement {
var out []Replacement
var seats struct {
Seats []struct {
Seat string `json:"seat"`
Scope string `json:"scope"`
Tools []struct {
Name string `json:"name"`
Description string `json:"description"`
Replaces []string `json:"replaces"`
} `json:"tools"`
} `json:"seats"`
}
if json.Unmarshal(tools, &seats) == nil {
for _, s := range seats.Seats {
for _, t := range s.Tools {
if len(t.Replaces) == 0 {
continue
}
out = append(out, Replacement{Address: s.Seat + "." + t.Name, NodeScoped: s.Scope == "node", Seat: true,
Does: firstSentence(t.Description), Replaces: t.Replaces})
for _, s := range seats {
for _, v := range s.Serves {
if len(v.Replaces) == 0 {
continue
}
out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == catalogue.ScopeNode, Seat: true,
Does: firstSentence(v.Description), Replaces: v.Replaces})
}
}
var listed []struct {
Module string `json:"module"`
On []string `json:"on"`
Replaces map[string][]string `json:"replaces"`
}
if json.Unmarshal(modules, &listed) == nil {
sort.SliceStable(listed, func(i, j int) bool { return listed[i].Module < listed[j].Module })
for _, m := range listed {
names := make([]string, 0, len(m.Replaces))
for t := range m.Replaces {
names = append(names, t)
}
sort.Strings(names)
for _, t := range names {
if len(m.Replaces[t]) == 0 {
continue
}
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
sorted := append([]ModuleTools(nil), modules...)
sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Module < sorted[j].Module })
for _, m := range sorted {
names := make([]string, 0, len(m.Replaces))
for t := range m.Replaces {
names = append(names, t)
}
sort.Strings(names)
for _, t := range names {
if len(m.Replaces[t]) == 0 {
continue
}
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
}
}
return out
@@ -190,36 +255,24 @@ func lookup(name string) []string {
return addrs
}
// AskMeshTools asks the controller now. A part that does not answer is left out and said in the error;
// what did answer is kept.
func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
// ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node placement names
// with its domains and addresses. A part that cannot be read is left out and said in the error; what was read is kept.
func ReadMeshToolsFrom(st States, seats []catalogue.Seat, resolve func(string) []string) (MeshTools, error) {
var m MeshTools
tools, terr := ask("seat:mesh-controller.tools", map[string]any{})
modules, merr := ask("seat:mesh-controller.modules", map[string]any{})
if terr != nil && merr != nil {
return m, terr
}
m.Replacements = ReplacementsOf(verbAnswer(tools), verbAnswer(modules))
nodesRaw, err := ask("seat:mesh-controller.nodes", map[string]any{})
placed, err := placements(st)
if err != nil {
m.Replacements = ReplacementsOf(seats, nil)
return m, err
}
var nodes []struct {
Name string `json:"name"`
}
_ = json.Unmarshal(verbAnswer(nodesRaw), &nodes)
for _, n := range nodes {
if n.Name == "" {
continue
}
machine := Machine{Name: n.Name, Domains: []string{n.Name + InternalSuffix}}
if shown, err := ask("seat:mesh-controller.node", map[string]any{"node": n.Name}); err == nil {
if d := publicDomain.FindStringSubmatch(outputOf(shown)); d != nil {
machine.Domains = append(machine.Domains, strings.ToLower(d[1]))
}
modules, merr := modulesTools(st, runsOn(placed))
m.Replacements = ReplacementsOf(seats, modules)
for node, p := range placed {
machine := Machine{Name: node, Domains: []string{node + InternalSuffix}}
if d := strings.ToLower(strings.TrimSpace(p.Decided.PublicDomain)); d != "" {
machine.Domains = append(machine.Domains, d)
}
seen := map[string]bool{}
for _, name := range append([]string{n.Name}, machine.Domains...) {
for _, name := range append([]string{node}, machine.Domains...) {
for _, a := range resolve(name) {
if !seen[a] && !net.ParseIP(a).IsLoopback() {
seen[a] = true
@@ -231,7 +284,7 @@ func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
m.Machines = append(m.Machines, machine)
}
sort.Slice(m.Machines, func(i, j int) bool { return m.Machines[i].Name < m.Machines[j].Name })
return m, nil
return m, merr
}
// KeepMeshTools writes what was asked to the module's state when it differs from what was kept, and
@@ -281,19 +334,19 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
}
// MeshToolsEvery is how often the controller is asked again.
// MeshToolsEvery is how often the bus is read again.
const MeshToolsEvery = 10 * time.Minute
// refreshMeshTools asks the controller at start and every few minutes, and renders when what it said changed.
// refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed.
func refreshMeshTools(p Paths) {
for {
m, err := AskMeshTools(ask, lookup)
m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup)
if err != nil {
say("asking the controller what each tool replaces: %v", err)
say("reading what each tool replaces and which machines the mesh has: %v", err)
}
if (err == nil || len(m.Replacements) > 0) && KeepMeshTools(p, m) {
if _, err := RenderNow(p, writeManaged); err != nil {
say("rendering what the controller said of its tools: %v", err)
say("rendering what the bus said of the tools: %v", err)
} else {
say("the mesh's tools changed: %d replace a command, %d machines; rendered", len(m.Replacements), len(m.Machines))
}
@@ -306,7 +359,7 @@ func refreshMeshTools(p Paths) {
func GuardStatus(p Paths, last int) map[string]any {
m := ReadMeshTools(p)
d := GuardDataOf(p, m)
asked := "never: the controller has not answered yet"
asked := "never: the bus has not been read whole yet"
if m != nil {
asked = m.Asked
}
@@ -696,7 +696,7 @@ type ProposalStore interface {
Get(key string) (json.RawMessage, bool, error)
}
// Recorder records a warrant in the controller's hand-act log: the controller reads the warrant from the
// Recorder records a warrant in the hand-act log of the seat mesh-hand-acts: the controller reads the warrant from the
// router's own record, never from this module.
type Recorder func(ask string) error
@@ -708,9 +708,9 @@ func (sdkAskBus) Publish(subject string, body any, id string) error {
func (sdkAskBus) Record(id string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", id) }
func (sdkAskBus) Kinds() ([]stdio.KindHeld, error) { return stdio.SeatKinds() }
// recordWarrant asks the controller to record a warrant this module acted on.
// recordWarrant asks the seat mesh-hand-acts (ADR 0565 clause 11) to record a warrant this module acted on.
func recordWarrant(ask string) error {
_, err := stdio.Ask("seat:mesh-controller.warranted", map[string]any{"asker": Module, "ask": ask})
_, err := stdio.Ask("seat:mesh-hand-acts.warranted", map[string]any{"asker": Module, "ask": ask})
return err
}
@@ -670,7 +670,7 @@ func TestTheRoutersRefusalIsSaidAndDiscards(t *testing.T) {
r.kinds = nil
refusing := &refusingRouter{router: r, p: p, store: store}
within, every := RouterAnswersWithin, RouterAnswersEvery
RouterAnswersWithin, RouterAnswersEvery = time.Second, 10*time.Millisecond
RouterAnswersWithin, RouterAnswersEvery = 30*time.Second, 10*time.Millisecond
t.Cleanup(func() { RouterAnswersWithin, RouterAnswersEvery = within, every })
_, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "x", Text: "Text.\n"}, store, refusing, t0)
if err == nil || !strings.Contains(err.Error(), "no channel can carry any of its answers now") ||
@@ -223,7 +223,7 @@ func TestTheRoutersRefusalIsSaidAndKept(t *testing.T) {
store := newMemQuestions()
refusing := &questionRefusingRouter{router: newRouter(), store: store}
within, every := RouterAnswersWithin, RouterAnswersEvery
RouterAnswersWithin, RouterAnswersEvery = time.Second, 10*time.Millisecond
RouterAnswersWithin, RouterAnswersEvery = 30*time.Second, 10*time.Millisecond
t.Cleanup(func() { RouterAnswersWithin, RouterAnswersEvery = within, every })
_, err := AskQuestion(p, mergeTonight(), store, refusing, t0)
if err == nil || !strings.Contains(err.Error(), "may not leave the mesh") {
@@ -303,7 +303,7 @@ func TestAReadWaitsNoLongerThanItsBound(t *testing.T) {
if err != nil || out["state"] != AnswerWaiting {
t.Fatalf("%v %v", out, err)
}
if took := time.Since(start); took > 2*time.Second {
if took := time.Since(start); took > 30*time.Second {
t.Errorf("a read asked to wait an hour took %s", took)
}
}
@@ -32,11 +32,11 @@
"withKey": {
"managed-mcp.json": "{\n \"mcpServers\": {\n \"anton\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"/a/b.js\"\n ],\n \"env\": {}\n },\n \"docs\": {\n \"type\": \"stdio\",\n \"command\": \"docs-mcp\",\n \"args\": [\n \"--x\"\n ]\n },\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n },\n \"search\": {\n \"type\": \"http\",\n \"url\": \"https://s.example/mcp\"\n }\n }\n}\n",
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true,\n \"apiKeyHelper\": \"/state/api-key-helper\"\n}\n",
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
},
"plain": {
"managed-mcp.json": "{\n \"mcpServers\": {\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n }\n }\n}\n",
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true\n}\n",
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ module claude-code
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030174137-a37181261c24
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require gopkg.in/yaml.v3 v3.0.1
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030174137-a37181261c24 h1:s0XXZtR7CFPBbnaDygWnE4/Q827iEFt0YfEzujk4Q9g=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030174137-a37181261c24/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
+7 -1
View File
@@ -22,6 +22,9 @@
"uses": [
"operator-channel"
],
"invokes": [
"seat:mesh-hand-acts.warranted"
],
"state": [
"servers",
"holdings",
@@ -32,7 +35,10 @@
"waits"
],
"reads": [
"claude-licence-manager.bindings"
"claude-licence-manager.bindings",
"mesh-delivery-assigner.placement",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests"
],
"tools": [
"claude_code_status",
+10 -4
View File
@@ -1,9 +1,9 @@
module claude-licence-manager
go 1.25.0
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
github.com/jackc/pgx/v5 v5.11.0
)
@@ -11,6 +11,12 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
)
+18 -6
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac h1:jnZ/n+fFMo7VQPNNXK1gjStHIJPw3SPbzPxsf/PpZqw=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -11,6 +11,14 @@ github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -18,10 +26,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
@@ -123,25 +123,27 @@ func TestTheLauncherUsesTheManagersDisplayAndWaitsOtherwise(t *testing.T) {
os.MkdirAll(fakeBin, 0o755)
// the manager's environment: empty until the file appears
os.WriteFile(fakeBin+"/systemctl", []byte("#!/bin/sh\n[ -f \""+dir+"/env\" ] && cat \""+dir+"/env\"\nexit 0\n"), 0o755)
run := func(env ...string) (string, bool) {
ctx, cancel := context.WithTimeout(context.Background(), 2500*time.Millisecond)
// wait is how long the launcher may run: a launcher that must answer gets a generous bound (the checker runs
// thousands of tests at once, issue 1814); one that must keep waiting is cut off after a short one
run := func(wait time.Duration, env ...string) (string, bool) {
ctx, cancel := context.WithTimeout(context.Background(), wait)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", file)
cmd.Env = append([]string{"PATH=" + fakeBin + ":" + os.Getenv("PATH"), "HOME=" + dir}, env...)
out, _ := cmd.Output()
return strings.TrimSpace(string(out)), ctx.Err() == nil
}
if out, ok := run("DISPLAY=:7"); !ok || out != "DISPLAY=:7" {
if out, ok := run(30*time.Second, "DISPLAY=:7"); !ok || out != "DISPLAY=:7" {
t.Fatalf("with DISPLAY: %q %v", out, ok)
}
if _, ok := run(); ok {
if _, ok := run(2500 * time.Millisecond); ok {
t.Fatal("it exited with no X server")
}
for _, n := range []string{"X0", "X1"} {
l, _ := net.Listen("unix", dir+"/x11/"+n)
defer l.Close()
}
if _, ok := run(); ok {
if _, ok := run(2500 * time.Millisecond); ok {
t.Fatal("it chose one of two servers")
}
// the session hands the manager its display while the launcher waits
@@ -149,7 +151,7 @@ func TestTheLauncherUsesTheManagersDisplayAndWaitsOtherwise(t *testing.T) {
time.Sleep(1200 * time.Millisecond)
os.WriteFile(dir+"/env", []byte("DISPLAY=:1\nOTHER=x\n"), 0o644)
}()
if out, ok := run(); !ok || out != "DISPLAY=:1" {
if out, ok := run(30 * time.Second); !ok || out != "DISPLAY=:1" {
t.Fatalf("the display that arrived while waiting: %q %v", out, ok)
}
}
+11 -2
View File
@@ -1,5 +1,14 @@
module clipmenu
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.7
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+11 -2
View File
@@ -1,5 +1,14 @@
module cups
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.6
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.6 h1:9qzdYONYbJdWcu6sxQcq9v1LI0JxcfkiKYkMUzJSkVQ=
git.novox.be/novox/mesh-sdk/go v0.1.6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+2 -2
View File
@@ -422,11 +422,11 @@ func TestRunWithoutABusSaysItStalledAndReconnects(t *testing.T) {
}, nil)
w.state = filepath.Join(t.TempDir(), "bus")
w.retry = 20 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
defer cancel()
done := make(chan struct{})
go func() { w.Run(ctx); close(done) }()
deadline := time.Now().Add(6 * time.Second)
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) && !w.Snapshot().Connected {
time.Sleep(50 * time.Millisecond)
}
+11 -2
View File
@@ -1,8 +1,17 @@
module dbus
go 1.22
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.7
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
github.com/godbus/dbus/v5 v5.1.0
)
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,4 +1,16 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+11 -2
View File
@@ -1,5 +1,14 @@
module deskchannel
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -8
View File
@@ -1,8 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.10 h1:EHhnPRrkw7/CSQAqRsxYnTvlTZ3dI02IPO6WSmVX80o=
git.novox.be/novox/mesh-sdk/go v0.1.10/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629 h1:7TYFV1Q6Mcz+lsLP/sds87tysET4rKCn3rRIQV8gNmA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d h1:HEJyqorKktrkJPeeJdwEo0kdrJjH3fm/YVS9MGftCqY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44 h1:r5xIo5qWOXQdCSMxvqJy8kV4U+P/QWXFK6971Ha6wU8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+2 -2
View File
@@ -181,10 +181,10 @@ func readingOf(s *memStore, key string) *health.Reading {
func waitFor(t *testing.T, done func() bool) {
t.Helper()
for end := time.Now().Add(5 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
if done() {
return
}
}
t.Fatal("not done within 5 s")
t.Fatal("not done within 30 s")
}
+1 -1
View File
@@ -3,7 +3,7 @@ module diskload
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406 h1:Z4RcH5Ft4OAAG3F538NXIHP9F3pkyk3j0LtP9mqu1qQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+11 -2
View File
@@ -1,5 +1,14 @@
module dmenu
go 1.22
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.6
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.6 h1:9qzdYONYbJdWcu6sxQcq9v1LI0JxcfkiKYkMUzJSkVQ=
git.novox.be/novox/mesh-sdk/go v0.1.6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+10 -1
View File
@@ -2,4 +2,13 @@ module dnsmasq
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+14 -2
View File
@@ -1,2 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=

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