Compare commits

...
Author SHA1 Message Date
jschoubben 125929dbc4 Pin mesh-sdk 05b3e2e1b94f and mesh-contracts ae5266e77639, the group's final heads, in every Go module (issue 1966)
mesh/delivery ready: it is merged on the Approve
mesh/check failed (7): shanks: novox/mesh-catalog#612 does not merge into the trunk: files in conflict: mod…; and 6 more | 125929dbc459 on bc4d43b508cf
mesh/delivery-group group feat/registry-milestone rejected: composed together: the checker's verdict at the members' heads (068b7d23cf1a): shanks
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:46:38 +01:00
jschoubben 6be8f984f7 mesh-vault: secret_own's description says it lists the own secrets of a machine, so a search for those words finds it (issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:42:41 +01:00
jschoubben fdc3331465 Pin mesh-sdk e5d89c5eedb5 and mesh-contracts 347a3053580a, the group's heads after review round 2, in every Go module (issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:25:28 +01:00
jschoubben 3498e54bdb Merge branch 'wip/registry-gcat-b' into feat/registry-milestone 2026-11-04 18:21:56 +01:00
jschoubben 2b340d7ddb mesh-conditions: a home for view-unsaid, every views owner's own
mesh-sdk's views.UnsaidFinding (id <owner>.<state>) is written by the indexer and the assigner in their own findings;
the closed set finds a catalogue writer of it too. nats.status-unwritten is said to the keeper directly, not a finding:
no row.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:18:23 +01:00
jschoubben b3261ccc03 G-catalog-B 6: on registry 2.8.3 a shared-digest tagged version's remove answers and says kept
Saying removed for a version the store still holds told the builder something false. remove now answers
kept with the reason shared-digest and the other tags naming its digest, removed false, and says the
seat's event kept with them (mesh-contracts' shared registry part); the builder raises no retry.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:15:24 +01:00
jschoubben da0573e124 nats: the manifest test reads spent read-only through the SDK's own decoding
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:14:43 +01:00
jschoubben 2b8b47d862 G-catalog-B 8: the retire removes only the teams the npm pair provisioner made
A person who later makes a team named packages or readers would have lost it at the next start. A team
counts as the provisioner's only in its organisation and when every member is one of its accounts (it
put its own accounts there alone, and they go with the first run), so a team made later stays.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:13:44 +01:00
jschoubben e97167c58f G-catalog-B 7: a transfer dropped by hand is recorded through mesh-hand-acts first
registry_incoming, npm_incoming and go_incoming dropped a transfer by hand with only a log line. Each
drop now records its hand act through the seat mesh-hand-acts before anything goes, as nats_requests
does; an act not recorded leaves the transfer open. distribution's manifest invokes the seat.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:13:44 +01:00
jschoubben 0d6d7acaae nats: a warrant's spend record in the keeper's own state spent; the fill pass while the consumer binds; partial says kept
Review round 2 of the registry milestone:
- security F2: the spend record leaves requests, which the tools write as the tool runner's account, for state spent,
  named in the keeper's bus.state and declared read-only (the lead's 18:1x ruling: the tools read it, never write);
  nats_requests lists the warrants spent from it;
- running F1: the fill pass also runs while the manifest-accepted consumer is bound again;
- health note 3: an event said on the seat's subject and refused by its history is kept with its one id and said again
  on the history alone, never again with a new id;
- seams note 3, health note 4: words unsaid with a status that cannot be written raise the keeper's own condition
  status-unwritten, cleared when it is written; the health names what a restart would lose.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:13:36 +01:00
jschoubben dc0349a220 G-catalog-B 5: distribution and gitea look at every open transfer at the idle watch's start
A transfer whose idle key ended while the bundle was down was never dropped. Both hand mesh-sdk's
FollowIdle their open transfers, which it gives Expired once the watch is taken.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:09:42 +01:00
jschoubben ccb7eaff13 Pin mesh-sdk c54648e74346 and mesh-contracts ec9529022c44, the group's heads, in every Go module (issue 1966)
Every module that imports mesh-sdk requires its group head; each requires the mesh-contracts seats and clients it
imports, and mesh-contracts/go through the SDK's seat list. The placeholder versions are gone.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:43:43 +01:00
jschoubben e958de14d2 Merge branch 'wip/registry-fcat-b' into feat/registry-milestone 2026-11-04 17:34:55 +01:00
jschoubben 00d6fe81bf F-parts: on registry 2.8.3 a shared-digest tagged version's remove keeps the tag and says removed
The store's pinned image is registry 2.8.3, whose DeleteManifest cannot delete a tag alone: by a tag it
deletes the empty digest and answers 400 DIGEST_INVALID. The fakes accepted a DELETE by tag, a fixture
agreeing with the mistake, so every merge of a reproducible build would have been a refused remove and a
retry finding live. RemoveTag now answers ErrNoTagAlone for that answer; remove then keeps the tag, whose
bytes stay under the other tag anyway, and says removed with that reason, so the builder lets it go. Any
other store failure is still refused; a digest shared with nothing is still deleted by digest. The fakes
answer as 2.8.3 does.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:30:50 +01:00
jschoubben f28d3bf286 F-parts: distribution and gitea bind mesh-sdk's idle key and binding cap instead of their own copies
Both holders carried the same idle bound and the same read of incoming's cap; the second instance of the
kind moved into mesh-sdk's Receiver (Arm, Expired, Bound, with BusArm, BusCap and FollowIdle). Each
holder now hands its idle state and its objects to those, and keeps its rows.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:26:11 +01:00
jschoubben f735392cb9 nats: the keeper wakes on decided, spends a warrant once, and says again what the bus did not take
Review round 1 of the registry milestone (design 25 § 12.2, § 12.3, § 12.7):
- the fill pass runs from the start, while the first view is asked;
- recreate and drop wake on the operator channel's decided on the keeper's durable consumer, never a minute's tick;
- a warrant is spent by compare-and-set (acted.<ask id> in requests) before anything is deleted; a second request on it
  is refused; a request whose ask ended unanswered is ended, said; the ask is raised before the request is handed;
- created is marked only once said; an event or a condition the bus did not take is kept, named by the health and
  said again at the next fill pass with its one id, on the subjects left (the double publish);
- the health judges the first view past its bound and never skips an unread consumer backlog;
- an unrecordable hand act is said on the request; nats_requests reads what waits, what ended and each warrant spent,
  and drops a waiting request by hand; a tool waits for its request's end on one watch of requests;
- the whole-module test reads the warrant from the router's record under a process connection.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:19:54 +01:00
jschoubben d320a19a8c F-catalog-B: the catalogue's Go is as gofmt writes it, and checks/gofmt holds it so
buildstep's generated.go and generated_test.go and claude-code's config_test.go reached the join
unformatted: the second of a kind, which no check caught since each module's tests hold only themselves.
All three are formatted, and checks/gofmt refuses a Go file of the catalogue gofmt would change.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:18:45 +01:00
jschoubben d8e43b48c4 F-catalog-B: no ignore pattern hides a module's source; nfs-server's and restic's patterns anchored
A bare pattern meant for a binary (mesh-build-node) matched the command directory of the same name, so git
ignored every new file there and publish.go was never committed (catalogue review F1). The kind: every
bare pattern of a module's .gitignore is anchored to the binary (nfs-server's hid all of cmd/nfs-server),
and checks/gitignored refuses an ignore pattern, the module's or the catalogue's, that hides a directory
under a module's cmd/ or internal/, a Go file added there, or a source file there, in a git checkout and a
plain folder alike.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:17:54 +01:00
jschoubben 64c893e2c2 F-catalog-B: the gitea module removes the npm pair's teams packages and readers and their accounts
ADR 0605 §5 revokes the npm write token, and the 14:04 deferral is withdrawn (security review F5): the
accounts the removed pair provisioner made (mesh_<node>_<consumer>) still logged in with write on the
organisation's packages. cmd/agent-teams, run at each start of the module and change of its teams value,
removes both teams and each account of that form whose only teams they are, never purging: one the forge
refuses to delete because it owns something keeps it and has its login prohibited.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:15:41 +01:00
jschoubben bccaf75556 F-catalog-B: npm_incoming and go_incoming read and drop the package seats' transfers; an idle one drops by its key
The package seats kept incoming, transfers and findings with no read or drop tool (tools review F4).
npm_incoming and go_incoming list a seat's open transfers, incoming's fill and the cap its binding carries,
and drop one by hand. The same fault as distribution's (running review F2, its second instance): the
transfers state's own lifetime dropped a transfer's key and left its chunks, and the drop otherwise waited
for the next publish. transfers keeps for good; each open transfer has a key in the new state idle with its
own time to live, whose end drops it. The holder counts against its binding's cap.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:14:04 +01:00
jschoubben 8f5611a2d1 F-catalog-B: registry_mirrors reads distribution's mirrors, asks one again and drops one by hand
The state mirrors had no read tool and no ask-again (seams review note 1): a copy a follower holds and
does not end could only wait for a restart. registry_mirrors lists each kept copy and its claim, asks one
again with its claim let go, and drops one, saying refused under the ask's id so its waiter ends.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:10:09 +01:00
jschoubben 1443adc4f0 mesh-conditions: observe node-tools' and claude-code's first views and the builder's publishing kinds
Their views findings were written and raised by nobody (ADR 0615 §6). The test finds every catalogue module
writing a views finding in the tree, so a new writer cannot go unobserved; homes for publish-refused,
publish-unanswered, seat-outcome-unread and family-not-heard.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:09:58 +01:00
jschoubben dce7d41671 F-catalog-B: distribution counts incoming against its binding's cap and drops an idle transfer by its expiring key
The holder counted against 20 GiB while the bus caps incoming at 8 GiB, so the bus refused writes before
the holder said refused naming the cap, and the 80 % finding came too late (running review F2). The cap is
read from incoming's binding (the object store's fill); IncomingCap, 8 GiB, is said only where none is read,
and the manifest's row holds it equal to max-mib. An abandoned transfer was dropped only on the next publish,
and the transfers state's own lifetime dropped its key while its parts stayed: transfers keeps for good, and
each open transfer has a key in the new state idle with its own time to live, whose end drops it (re-armed
for the rest when it took a part since). registry_incoming reports the bound cap.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:09:17 +01:00
jschoubben 6f89e622df F-catalog-B: distribution's windows step by mesh-sdk's delivery.StepWindow and say why a node holds one
A read naming no node (an empty node_haves) closed both windows for good (security review F3, second of
a kind): the window embeds delivery.WindowHeld and steps by the shared rule, named being every node the
read named, and keeps its own guard that a window once closed stays closed. A node-engine whose
manifest-reads did not answer (a timeout included) counts as stating nothing as before, and the window's
record now says so per node (Why), so registry_old_name tells a timeout from an old node-engine. A read
that fails otherwise is an error the wake judges nothing on, no longer taken as not served.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:07:10 +01:00
jschoubben 3b01711e1f mesh-config: the whole test waits on the module's views subscriptions, never a second's sleep
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:05:55 +01:00
jschoubben 1b485eb2aa F-catalog-B: distribution's front takes no bootstrap credential
The lead's ENG-5 ruling: on a fresh mesh the bootstrap pushes the holders' own images to the store on its
loopback with no credential, so the front's bootstrap token, its file, the bootstrap-push window and the
program's removal of the file go. Fewer secrets; nothing to refuse once a window closes.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:05:24 +01:00
jschoubben 1bdca045c2 F-catalog-B: distribution removes a tagged version's tag alone when another tag names its digest
Deleting the manifest by its digest removed every tag on it, and reproducible builds share a digest
between a branch's tagged version and the trunk's numbered one, so a branch's end deleted the trunk's
image (security review F1). The fake catalog now deletes by digest as the store does; a store that
cannot delete a tag alone removes nothing and the removal is refused.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:04:46 +01:00
jschoubben 70fd02e8da claude-code: drop a committed go.work.sum, a local workspace leftover
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:04:07 +01:00
jschoubben 3079b7f115 checks/manifests refuses a manifest giving one field twice
A decoder keeps the last of two equal keys without a word, so a merge
of two branches that each add a field to a manifest loses one side
silently; the indexer's judge reads the same bytes. Rows: a top-level
field and a field inside an artifact, each named by its path.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:45:23 +01:00
jschoubben dee54a1a13 Join: mesh-build-go's manifest gives invokes once; gitea's row names all three
A3's merge auto-merged a second invokes into mesh-build-go's manifest
(the registry seats' publish verbs beside the indexer's read tool); a
decoder kept the last and dropped the read tool, which
TestTheModuleReadsTheIndexersManifests caught. One list holds both.
gitea's package-registry row named only the builder's versions; the
manifest also asks the indexer's modules and records a re-read.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:45:23 +01:00
jschoubben 0554f10c0a Join: mesh-knowledge's page address is no word of the environment
The overview page read MESH_KNOWLEDGE_PAGE, which nothing in the mesh
sets; only its bus test did, to serve no page. checks/environment,
which came with A3's merge, refuses such a read. The page listens on
the port its manifest declares; the test's bundle sets the package's
address empty instead.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:43:28 +01:00
jschoubben 019184cf5d Join: merge origin/main (#611, a closing reviewed on its closing alone)
Conflict in mesh-knowledge's reviewOne: the join judges a record in
parts, #611 asks a closing only of the records its judged links name,
with the status it moves from. Both kept: a closing is one part,
ClosingPart, asked #611's ClosingQuestion over the named records.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:39:22 +01:00
jschoubben 69ceac7fcf Join: merge A3 (wip/registry-a3)
Conflicts, both sides kept: artifact-store-tools' invokes (the assigner's
and indexer's read tools, followed through views, beside the registry
seats' hold, release and remove), its main (the views reader and the
seat asker), its manifest row; gitea's invokes (the indexer's read tool,
the hand act and the builder's versions) and its consumes of the
indexer's views. gitea's account test takes the packages account's
names; repoCheckFacts, removed on main, is not imported. No catalogue
module reads the indexer's state.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:38:20 +01:00
mesh-tools 8acad77d33 Merge pull request 'mesh-knowledge: a closing is reviewed on its closing alone, a graduation on every became link it carries (issue 784)' (#611) from fix/784-closing-question into main 2026-11-04 15:13:55 +00:00
jschoubben 46505d42d8 A graduated closing is judged on every became link the effort carries, carried ones too, each with its record's title and status (issue 784, ADR 0630 §2)
mesh/delivery merged: source.main moved to novox/mesh-catalog@8acad77d33f9
mesh/check being carried to 46505d42d8fb on 8acad77d33f9 | from d2f326dff40e
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 16:00:13 +01:00
jschoubben a198a959fe A closing is reviewed on its closing alone: the question holds the move, the note and the added links, never old text or linked clauses (issue 784, ADR 0630)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 15:55:58 +01:00
jschoubben 85d96d327f J7: the gate reads the ask it keeps through operator-channel's client (occ.Carried)
askOf keeps its key step and holds the carried ask to its id and an option with
occ.Carried, the one reader the assigner, teller and view use; its own decode, id and
option checks are gone. Rows in TestTheGateRefusesAnAskWithAnotherIdOrNoOption name the
client's refusals and a value carrying no ask; they failed with the gate's own checks.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:49:38 +01:00
jschoubben 5601fdb51d Join: merge origin/main (#610, the knowledge base's close verb)
Conflicts in mesh-knowledge: main.go Verbs and manifest_test.go, A6's draft verbs and
#610's close, both kept in the manifest's order; review.go reviewOne, A6 judges a version
in parts and #610 judges a closing on its closing alone: a closing version is one part
(ClosingPart) asked ClosingQuestion. Joined: close.go's asks import by the rewrite map,
and its rules probe names its overview section "Overview", since A6's section-form rule
refuses a file-named section (close's test failed on it).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:48:32 +01:00
mesh-tools d2f326dff4 Merge pull request 'The knowledge base's close verb: a research effort graduates, is abandoned or parked as an idea, judged only on its closing (issue 784, ADR 0630)' (#610) from fix/784-close-verb into main 2026-11-04 14:43:58 +00:00
jschoubben 8eb4465590 Join: mesh-knowledge's progress bus test counts the page's first view
At the joined SDK the bundle is served in the test, so ServePage's first view asks the
tracker's list and milestones and mesh-delivery's groups once beside progress (checked:
with the page's start skipped the counts are the old 3, 1, 3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:42:35 +01:00
jschoubben aef8b9c7cb Join: mesh-config's whole test reads the subject the runtime publishes on from the manifest
The seat definition names mesh-config's renamed, retiring and retired on its own stream
(mesh.stream, A1c), and the runtime publishes an event the module's stream carries
there; the test assumed EVENTS (mesh.mod) for every event and failed at the joined heads.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:40:33 +01:00
jschoubben 7dd9e11239 Join item 6: no compiled program in the tree, and the health count written down
A8's CAT-RD4 and CAT-RD8 committed the mesh-conditions and minio binaries
(checks/nobinaries): removed and ignored. The long-running resources without health fell
to 57 in the joined tree (checks/health ratchet).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:38:12 +01:00
jschoubben aa5e2a0dd6 Join item 6: gitea announces its findings readable, which the condition keeper reads
A8 gave gitea a findings state and the keeper its read; gitea's announcement did not name
it, so the read was refused on the mesh (checks/readable) and its own kept test failed.
Rows in test/kept.test.ts; they failed before the change.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:38:12 +01:00
jschoubben 4a2555253c Join item 1: claude-code requires the mesh-contracts modules it imports, for the group pin
Its seat set (mesh-contracts/go, A6's CC2) and the operator-channel contract and client
were imported with no require; the root workspace hid it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:34:11 +01:00
jschoubben 34d5c0d308 Join item 2: mesh-knowledge's ledger pipe, call ids and page heartbeat come from the SDK
The ledger pipe asks the runtime through stdio.BusState (it answered ErrNoPipe before), an
operation names the call the runtime proved (stdio.CallIDOf), and the page takes the
runtime's word on its consumer (stdio.OnHeartbeat): a beat is every subscription heard, a
missed heartbeat marks each subscribed source at once with the runtime's why. Rows in
TestASourceThatDoesNotAnswerIsSaidAndAskedAgainOnABackoff and
TestTheLedgerStoreRefusesWhatTheLedgerWouldHaveToTakeBack; each failed before the change.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:33:35 +01:00
jschoubben 695ac0356d Join item 1: the catalogue's modules take the router's ask from operator-channel's contract and client
mesh-sdk holds neither asks nor asker, nor the vault's answer types, and mesh-delivery's
sdk/views is mesh-sdk's go/views (a1-progress.md, the rewrite map for the join): thirteen
modules switch to the operatorchannel contract and client, mesh-sdk's channel package and
meshvault, with go.mod requiring each at the group pin (nats: meshbroker too). The gate
holds the checks mesh-sdk dropped (an ask's id is its key's asking, at least one option),
with a row that fails without them; claude-code refuses a router record whose times do
not parse, as its JSON decode did, with a row.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:33:27 +01:00
jschoubben 60a187cfa8 Join item 3: the condition keeper reads every finding the registry milestone added
The builder's four kinds, the indexer's four, the package seats' token refused and the
owners' view too large get their homes; the keeper reads the findings of every reader
of the indexer's and the assigner's state (teller, enrolment, builder, checker, view),
and asks the indexer's findings of its read tool and follows its views events, as the
assigner's (ADR 0615 §1). Rows in TestEveryControllerKindHasItsHome and the manifest
test; each failed before the change.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:19:13 +01:00
jschoubben e9ba05bd9a close: read the became links the effort already carries, check the record size, drop empty table cases (issue 784, ADR 0630)
mesh/delivery merged: source.main moved to novox/mesh-catalog@d2f326dff40e
mesh/check being carried to e9ba05bd9a3c on d2f326dff40e | from 79c2fc7fe1d5
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 15:15:34 +01:00
jschoubben 10d53dd02b Join: merge A2 (wip/registry-a2) 2026-11-04 15:12:01 +01:00
jschoubben 89d0eb13af Join: merge A6 (wip/registry-a6)
Conflict in mesh-knowledge main.go, the propose tool's description: #609 (79c2fc7f)
names ideas and the research-active-at-most limit, A6 names a research effort's
partial new version; both sentences kept, A6's rule count (31).
2026-11-04 15:12:01 +01:00
jschoubben 69303e677a Join: merge A5 (wip/registry-a5) 2026-11-04 15:11:21 +01:00
jschoubben 84ab1fe80d Join: merge A8 (wip/registry-a8)
Conflict in mesh-conditions keeper.go Observers and module.json reads: origin/main
added mesh-vault's findings, A8 added the first-view readers' findings; both kept.
2026-11-04 15:11:21 +01:00
jschoubben 6061c775d5 Join: merge A4 (wip/registry-a4) into the registry milestone 2026-11-04 15:11:07 +01:00
jschoubben 8f5667cca5 The knowledge base's close verb closes or parks a research effort, judged only on its closing (issue 784, ADR 0630)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 15:10:36 +01:00
mesh-tools 79c2fc7fe1 Merge pull request 'The knowledge base keeps the roadmap: a research effort can be an idea, and at most three are active (issue 1970, ADR 0629)' (#609) from fix/1970-research-ideas into main 2026-11-04 13:57:21 +00:00
jschoubben 55a88cdba4 Drop a holds test that passes without its change; the propose description names the limit's setting
mesh/delivery merged: source.main moved to novox/mesh-catalog@79c2fc7fe1d5
mesh/check being carried to 55a88cdba4e5 on 79c2fc7fe1d5 | from 7fe569ef4067
Round 2 of issue 1970's review: a research effort has no clauses, so holds never serves one; Served on an
idea is tested in TestAnIdeaIsListedAndNeverServedAsTruth.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 14:34:40 +01:00
jschoubben 29f6039fb1 Research limit: a new version of an active effort is never refused; holds and tool text follow (issue 1970, ADR 0629)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 14:32:27 +01:00
jschoubben b0a275c126 Research efforts can be ideas, and at most three are active (issue 1970, ADR 0629)
The roadmap lives in the knowledge base: an idea is listed, never served as in force; propose refuses a further active effort.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 14:24:44 +01:00
jschoubben d31b41bb7e DLV-C3r3 (Node): name the packed group package the old form, kept while the running checker hands it
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:36:14 +01:00
jschoubben ed7c6c4fb6 DLV-C3r3 (Node): mesh-build-node places a group's tagged version of the SDK from the registry
ADR 0607 §3, §5: the fetch's package argument also takes {name, version}; the version is placed from the registry in
each TypeScript member whatever its lock names, checked against the lock's integrity when the lock names it, else the
registry's; nothing is packed then, and a version the registry lacks fails naming it. The packed {repository, commit}
form stays for a checker that hands it. Rows in the placement and argument tests.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:23:10 +01:00
jschoubben cd66db54bf DLV-C3r3 (Go): mesh-build-go fetches a group's tagged version by that version
ADR 0607 §3, §5: an asked semantic version (v0.1.15-feat-x.2) is downloaded through the Go seat and built whatever
go.mod names, said when go.mod names a newer one; one the seat does not hold fails naming it as a download that may be
retaken, never looked for among the pull request heads. Rows in the asked-version table test.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:23:10 +01:00
jschoubben 45688f196e CAT-N7: the seat mesh-broker's reading verbs answer the seat's contract
list, show and orphans answer meshbroker.ListAnswer, Held and OrphansAnswer; check answers CheckAnswer with each
differing entry's settings asked and held by stream and whether the difference only grows; show takes the contract's
local-name. Built against mesh-contracts' seats/meshbroker module in the workspace; the group pin adds its require.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:10:43 +01:00
jschoubben d7214bb576 CAT-N7: the keeper says each of mesh-broker's events on the seat's subject, and keeps it on its own stream for history
The lead's ruling of 13:0x (registry milestone): mesh-broker's events are mesh.seat.mesh-broker.event.<event> on the
shared events stream like every seat's, and the bus module's own 30-day stream keeps each for its history tool.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:58:44 +01:00
jschoubben ee54bae09f CAT-N8: the keeper's rows for each refusal ADR 0626 §4 names: a drop naming no ask, a warrant at another level or from another asker, a create no accepted manifest asks for, a snapshot that fails; each said why and deleting nothing
The keeper already reads the router's record of the ask on its own account and verifies the
warrant itself (bound to this act on this entry, at the destroy level), so only the rows are new.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:49:55 +01:00
jschoubben 3daedac1bb CAT-N1: the keeper's bus declares keeps, the one key more ADR 0626 §1 allows the bus module's keeper
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:46:10 +01:00
jschoubben 313c1997a4 CAT-CC3: claude-code reads the indexer's modules and manifests and the assigner's placement from their first views and views events, never their state (ADR 0615 §1)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:45:58 +01:00
jschoubben e6806d5698 CAT-CC2: the instructions' seat table reads the seat set mesh-contracts compiles, not mesh-sdk's catalogue
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:43:55 +01:00
jschoubben d0f384610c CAT-K6: the manifest test no longer expects claude_code_judged among the invokes
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:42:49 +01:00
jschoubben 8d4b2cb1c6 CAT-K6: a review waits for the agent module's judged event under its ticket; claude_code_judged is no longer invoked
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:41:50 +01:00
jschoubben 4f433bdaf8 CAT-CC1: a judgement asked with a ticket says one judged when it ends, with its answer or why it failed
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:39:58 +01:00
jschoubben 91cd016f65 CAT-MS1, CAT-MS2: offered and closed carry the operator-channel definition's fields (id, asker, headline, channels; id, asker, how); the page reads them
The test of the five ends holds every key of a body to the seat definition's holder events, so the router and the contract cannot drift.

Issue 1968.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:35:35 +01:00
jschoubben cdddeee4ea CAT-N3: the events' body in the field names of the seat mesh-broker's message said
module, local-name, storage-type, template, options, configuration, bus-name,
asked, held, fill, snapshot and why as the seat's definition in mesh-contracts
names them, so taking its generated type changes no byte; binding (a ledger's
several names) and revision (the indexer's, of the verdict acted on) beside,
asked of the definition.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 3e019c5d7e CAT-N10: gofmt the integration test
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben fa4ccbc0a9 CAT-N10: the bus module's one whole-module integration test, against the real bus server
Design 25 § 12.7's rows, as subtests of one test on one server (CAT-N10): a
verdict, then created on its own stream BROKER_EVENTS, the objects on the bus;
a restart catching up a verdict given while it was down; a replay older than
the first view not applied and a module missing from the first view keeping
its key; a drift said, a growth applied and the drift cleared; a refused
create with no created; a log at a small cap refusing, full raised and cleared;
an orphan dropped only on the operator's warrant, after a snapshot written
whole, recorded as a hand act; nats_first_view_again's hand act.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 76aab05289 CAT-N2b: nats_first_view_again asks the indexer's first view at once, a hand act
The bus module's tool hands the ask to the keeper through its own state
requests (design 25 § 12.2), as the caller the tool runner proved; the keeper
asks indexer_bus_state at once beside its backoff, applies what it answers,
records the hand act and writes back what it answered and at which revision.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 4b2d5aeaab CAT-N6: the keeper's health tool nats_keeper_health, which the process keeper's tool check asks
It reads the keeper's status from its own state asked, what the keeper holds,
and how many accepted verdicts wait on its consumer <node>_nats_keeper on
EVENTS from the monitoring endpoint, and answers HealthOf's judgement:
unhealthy, naming why, past the three-minute bound; the entries refused,
waiting and past the raise mark named beside.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 68dd69f4ef CAT-N9: the old remove tools refuse an entry of the new form, naming nats_drop
The mesh deletes a module's kept data only through drop and recreate (ADR
0595 §4 as its insight has it; design 25 § 12.3): nats_stream_remove,
nats_bucket_remove and nats_consumer_remove read the keeper's own state asked
and refuse a stream or bucket the keeper made, before the bus is reached,
naming nats_drop and nats_recreate; one that cannot be told apart is not
removed. The old sections' entries are removed as before. Rows of the
existing refusal test.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 3b61a05532 CAT-N8: a person's create, apply, recreate and drop are handed to the keeper as the caller the tool runner proved
The bus module's tools hold no grant of the seat's acting verbs (design 25
§ 12.3): nats_create, nats_apply, nats_recreate and nats_drop write a request
into the keeper's own state requests, stamped with stdio.CallerOf (a name in
the arguments is never the caller), and read its end back. recreate and drop
raise the operator's ask first, its option binding exactly that act; the
keeper acts only on the router's warrant for it (Warrant.For, Option.Performs),
after a snapshot of the entry's streams (bussnap, moved from the snapshot
command with a streams filter), says recreated or dropped, and records every
act as a hand act through mesh-hand-acts. A drop removes an orphan alone.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 6be2e91da8 CAT-N7: the seat mesh-broker's reading verbs list, show, check and orphans, and nats_history
Served by the bus module's bundle from the keeper's own state asked and the
bus's monitoring endpoint (design 25 § 12.3): each entry's template, bus
names, settings asked and held and fill against its cap; check names each
object missing or differing; orphans what no accepted manifest asks for.
nats_history reads one entry's events, in order, from the bus module's own
stream BROKER_EVENTS on the tools' credential. The manifest test now holds
every tool and reading verb the manifest names to what the bundle serves.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 905c34a537 CAT-N2: the keeper's process: the first view from indexer_bus_state, then manifest-accepted on its durable consumer
nats-tools keep is the process keeper, on its own account (own secret
broker-keeper): it asks the indexer's indexer_bus_state once, on a backoff
from 10 s doubling to 5 min while it does not answer (unhealthy meanwhile,
nothing created or orphaned), then binds its durable consumer on EVENTS
(<node>_nats_keeper, the indexer's manifest-accepted) and applies each verdict
only if newer by the indexer's revision; a manifest-accepted carrying no
revision yet is read as 0. A fill pass every minute, waiting entries made
again on the same backoff. It says its events on its own stream and its
conditions through the condition keeper, and serves nothing (ADR 0436 §3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben cc1a942235 CAT-N6: the fill pass raises full at the raise mark and clears it below the clear mark; the keeper's health
The keeper's one periodic read (ADR 0595 §6): every entry that refuses when
full, by its fullest object; full raised at the raise mark (75 by default),
cleared only below the clear mark (70), so an entry at the mark does not flap;
an entry dropping its oldest is never read. HealthOf is the keeper's health
for its tool: unhealthy when its pass is more than three minutes old, its
first view has not answered (naming the indexer's verb), or verdicts wait on
its consumer unanswered past the bound; it names the entries refused, waiting
and full beside.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 93bfdd9e2f CAT-N5: orphans: an empty accepted manifest asks nothing, so what was made is orphaned and kept until a drop
An entry made for a module whose newest accepted manifest no longer asks for
it is said orphaned and raised, never removed; asked again it is kept, its
binding said again with created and the condition cleared. A module missing
from a first view keeps its key (a set-aside manifest is not in it); an entry
never made leaves nothing, so the empty verdict ends that module's key.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 1f68f80760 CAT-N4: check and drift: a difference that only grows is applied, any other said and raised until cleared
Rows of the keeper's table test: a larger cap is applied with no event; a
smaller one is drifted with what differs, raised once, and drift-cleared once
the manifest asks what the bus holds again; an entry the bus already holds is
taken as made and its binding said, nothing made twice.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben ff4d042be5 CAT-N3: the keeper creates each template's bus objects under the module's names and says created or refused
Each bus-state entry is planned into its objects (design 25 § 12.1): state a
bucket <module>_<local>; events, work queue, log and a bare stream a stream
<module>_<local> on mesh.bus.<module>.<local>.>; objects an object store;
a ledger its history stream, its state and index buckets and its snapshot
stream, one message per subject. The keeper makes what the bus lacks on its
own account; a refusal of its grant waits for the next check, a refusal of the
entry is said (refused) and raised, cleared by a later create. The comparison
of what is held and the orphans are in the same pass (CAT-N4, N5).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 4f24cb0751 CAT-N1: the bus module's keeper, its own events stream, state and the seat's reading verbs in the manifest
The keeper is a process of the tools bundle on its own account (own secret
broker-keeper, ADR 0436 §1): it hears the indexer's manifest-accepted, keeps
what it heard in its old state asked and the persons' requests in requests
(ADR 0595 §10), and says its nine events on its own stream broker-events,
30 days at 256 MiB (design 25 § 12.3, ADR 0620 §2). mesh-broker serves its
four reading verbs; its acting verbs are the keeper's own work. The server
gains a tcp check, which the keeper's tool check stands beside.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00
jschoubben 0d86833123 CAT-MS1: the router says when an ask is offered and how it ended
offered once when an ask is offered, closed once for each of the five ends (answered, expired, cancelled by its asker, replaced, ended by the router), with the key, asker and headline and never an option, ticket or proof; on a stream of the events kept seven days, 64 MiB, granted by the trusted value readers, which names the knowledge base alone. Issue 725's runtime change is delivered (resolved 2026-10-26).

Issue 1968.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:18 +01:00
jschoubben 715bc088c4 CAT-P3: the page's model is a tool, its re-read a hand act, an unanswered source is said and asked again on a backoff
knowledge_overview answers the page's model as data (reads); knowledge_overview_reread asks one source's verb again by hand, no other, and
records the hand act (acts). A source marked from its verb is asked again alone from 10 seconds, doubling to 5 minutes, its first answer clearing
the mark; one marked from its subscription asks nothing while silent and the verb once on its next event. The module's health carries the page's
last rebuild and says it is degraded while a source is unanswered, staying healthy.

Issue 1968.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:31:55 +01:00
jschoubben ab5f767372 CAT-P2: the overview page: a handler that answers GET only, on a port for this machine only, through a required route
The module's bundle (the one process with the bus) serves the page on the port its manifest declares under listens, from: machine,
and requires a route that reaches it. No form, no script, no verb behind it; any method but GET is 405. A source that does not
answer is said on the page, never shown as an empty list.

Issue 1968.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:30:23 +01:00
jschoubben 40b1307839 CAT-P1: the overview page's model: three blocks from the ledger and three sources' verbs and events
The model is built once at start from the issue tracker's milestones and list, the delivery groups and the operator channel's asks, and
kept in memory from their events (group, transition, merged; offered, closed; opened, moved, milestone-set, milestone-moved), asking no verb
again; the records come from the ledger and the seven days are counted when the answer is given. The manifest invokes groups and asks,
consumes those events, has no reads and no decided family.

Issue 1968.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:28:32 +01:00
jschoubben a3a11842bf CAT-K13: the knowledge base runs whole on the ledger over a real bus, and a restart finds every record
One integration test: the module's states are JetStream buckets, its events are published under their message ids to a stream
that drops a repeat; it moves its records, proposes, is reviewed and accepts a record, loses an event between an operation and its
send, restarts, sends the lost event exactly once, builds its index and checks the database within 40 seconds, and is healthy. The
test found that a restart probed the ledger number by number and stopped at a gap of 50, finding 3 of 14 records: the store now
keeps a key held.<kind>.<number> in the state findings for each record it creates and reads them at start (the old probing, with a
gap of 1000, only for a ledger written before the keys).

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:24:38 +01:00
jschoubben beba03613c CAT-RD2: gitea says an unanswered first view in its findings and records the re-read through mesh-hand-acts 2026-11-04 12:24:11 +01:00
jschoubben d8e8c258a6 CAT-RD1..RD10: the readers use mesh-sdk's views.Reader, say an unanswered first view in their own findings (mesh-conditions observes them) and record a re-read through mesh-hand-acts 2026-11-04 12:23:37 +01:00
jschoubben c73641c117 CAT-K12: the health on the ledger names the source and the move's last check, and fails for what the ledger owes
Unhealthy, naming the cause, for a ledger or the verdicts' stream that does not answer, a head behind its history, an index behind
the ledger and a stream at its cap; the idle drafts, the parts waiting on the judge and the records the form-only check refused
are counted and leave it healthy. A part waiting more than 30 minutes is asked of the operator once, cleared when it is judged.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:21:51 +01:00
jschoubben 47b3ae7528 CAT-K11: form-only versions: plain titles and links of the one form, proven to change form alone
A record imported with file-named sections gets plain titles, and its links to files become references of the one form, in one
version written after the move by the tool knowledge_form_only: marked form, listing the names changed, accepted without a
judgement because a check proves each section's text with its links replaced by their labels is the same and each new link names
the record the old one named. The old names stay as aliases get takes; the parts' reviews carry over; a record waiting for its
review is skipped.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:20:19 +01:00
jschoubben e5e3d7dce6 CAT-K10: the module's tools read each state it keeps on the bus and do by hand what it does itself
knowledge_head, _operations, _section, _draft, _parts, _asked, _fill, _rebuild_index, _review_again, _integrity,
_move and _move_check, beside the health, highest and marks tools; the manifest lists them and a test compares them with the code.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:18:11 +01:00
jschoubben 5adb8e1fea CAT-RD10: the deprecated mesh-catalog module reads none of the indexer's state; it registers builds without manifests, said once (ruling U4) 2026-11-04 12:16:37 +01:00
jschoubben 2bf608fc5f CAT-K9: the move to the ledger: a dry run, the copy as operations, the check, the switch key, the database written beside it
A movableStore serves the database until the switch key says the ledger. The move copies every version of every record as an
import operation (the old author kept apart, marked imported; the ledger's author the module's account), refuses writes for its
length, checks record by record, and sets the switch only on a clean check. From then on the ledger is served and the database is
written beside it, behind or not (database.sequence, database.behind); the check runs at every start before the drop, and the
search index is built then.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:15:52 +01:00
jschoubben 48624da9e3 CAT-RD2: gitea asks the indexer's modules once and follows its events (TypeScript follower), no reads; mesh-build-go's manifest text kept as it was 2026-11-04 12:14:56 +01:00
jschoubben 25e5d00aba CAT-V1: retire takes ask beside warrant, as the seat definition now has it
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:12:54 +01:00
jschoubben a4e3a9cbe0 CAT-K8: the search index is in the module's memory (bleve), built from the ledger and kept as it writes
ask and holds on the ledger answer from a full-text index of the heads and the current versions' text and clauses; it is
built at start (index not built is the answer meanwhile), a new version replaces its record's documents, a reserved,
withdrawn or retracted record has none. A new pure Go dependency, bleve.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:12:44 +01:00
jschoubben dd120b1c90 CAT-RD3: mesh-build-go asks the indexer's read tool for the manifest a compose ask names, no reads of the indexer 2026-11-04 12:12:01 +01:00
jschoubben 6b929c9057 CAT-V3: the vault's unanswered first view of the placement is a finding the condition keeper raises
mesh-vault gains the state findings (old section); past two minutes without the assigner's first view it writes
first-view-unanswered naming the owner and the tool, the answer deletes it. mesh-conditions reads mesh-vault.findings
and holds a row for the kind.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:11:46 +01:00
jschoubben f49b6de79b CAT-RD5: mesh-provisioner asks the assigner's placement and wants and the indexer's modules and manifests once and follows their events, no reads 2026-11-04 12:11:18 +01:00
jschoubben 4d05e0d290 CAT-RD4: mesh-conditions asks the assigner's wants, placement and findings once and follows their events, no reads (on CAT-C10's base) 2026-11-04 12:10:09 +01:00
jschoubben 972ecd105d CAT-C11, CAT-V3: follow A2's views follower (Changed also says when)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:09:35 +01:00
jschoubben 7bf0e991d8 CAT-K7: every operation's event is sent once, after a restart too
An event of a change carries the record, the version it follows and the event as its message id; the last version sent per
record is a key of the state sent; at start the events of the operations after it are sent again under the same ids, so a
stop between an operation and its event loses none and a second start sends none.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:09:26 +01:00
jschoubben 2bb964b000 CAT-RD1: artifact-store-tools asks the assigner's wants and the indexer's manifests once and follows their events, no reads 2026-11-04 12:09:12 +01:00
jschoubben 692f18ef3b CAT-C11: mesh-config reads the indexer's and the assigner's state through their tools and views events
Six states (modules, manifests, predefined, config-candidates, placement, wants) are asked of the owners' read
tools at start and kept from their views.<state> events, a gap asks again; a reader started while an owner is
down keeps asking and writes a finding past two minutes. The builder's builds has no owner tool and stays a
read. The returning node-engine's event is read by its node token (ADR 0618 as built). Tools
config_first_views and config_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:08:56 +01:00
jschoubben 6128a570a2 CAT-RD6: mesh-self-check asks the owners' placement, seat-holders, modules and manifests once and follows their events, no reads 2026-11-04 12:08:14 +01:00
jschoubben c5dc10fa8c CAT-K6: a version is reviewed in parts: the head and each section, a part judged again only when its text or what it names changes
One judgement per part with no tools, at most two at once; a section is read with its frame (the clauses and records it
names and the digest of its Context), so a usual revision judges only what changed. The latest approval of each part is
kept in the head and carried to the next version; a part that could not be judged keeps the others' verdicts and is the
only one asked again; reviewed is said once per version and each verdict as part-reviewed on the stream reviews. A part
in flight is a key of the state asked. A version is accepted when every part approves; findings name their part.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:07:32 +01:00
jschoubben 55eecc2d34 CAT-RD9: nfs-server asks the assigner's seat-holders once and follows its events, no reads 2026-11-04 12:07:07 +01:00
mesh-tools 7fe569ef40 Merge pull request 'A failed backup names its cause: nats's dump keeps the snapshot's exit status and the container's state, restic reports the tail of what a command printed (issue 1585)' (#608) from fix/1585-dump-stderr into main 2026-11-04 11:06:34 +00:00
jschoubben 3e3294cd47 CAT-RD8: minio asks the assigner's seat-holders once and follows its events, no reads, no cache on a timer 2026-11-04 12:06:34 +01:00
jschoubben 40e1e087e5 CAT-RD7: mesh-watcher asks the assigner's seat-holders once and follows its events, no reads 2026-11-04 12:05:51 +01:00
jschoubben e1a182bf09 CAT-C10, CAT-V1: the modules' readmes say the rename, the retirement and retire
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:59:46 +01:00
jschoubben 553cf50da7 CAT-V4, CAT-C10: the vault's retire and the config events and renames verb match the seat definitions of mesh-contracts
retire takes the definition's warrant (the hand act's id, naming node, module, name and ask) and answers
retired; own-retired, renamed, retiring and retired carry exactly the definition's bodies on its subjects.
The whole-module tests decode what the module published strictly into the generated types. The modules
require mesh-contracts, to be pinned with the group (a local replace in an uncommitted go.work until then).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:59:24 +01:00
jschoubben bd9764ef9a CAT-K5: a section is a part with a plain title, never a file; a reference has one form
A new rule, section-form (31 now), refuses from the day the records move a section whose title is a file name, a number
prefix, a slash or over 200 bytes, a title twice in a record, and a text that holds a local path, a file and line, or a
link to a file. Only a section that is new or changed is judged, so a record imported with file-named sections keeps
them until its form-only version; write refuses at once.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:56:31 +01:00
jschoubben bb85684b2a CAT-V1: the seat mesh-vault's retire removes one module's own secret on the warrant of mesh-config's hand act, and says own-retired
The caller must be on the vault's trusted list retire-callers (the holder of mesh-config alone) and carry
a hand act it recorded for exactly this node, module and name. A waiter on rotated or own-replaced does
not wake on own-retired. Reviewer note of issue 1966: HandedOf returns a placement read error instead of
dropping the modules placed there.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:54:58 +01:00
jschoubben 17ba95bca6 CAT-K4: a record is written in a draft, one section a call: open, write, remove, order, submit, drop, take-over
A draft is a key of the state drafts (author, fields, sections as title, digest and the sequence of their write-section);
write puts the text into the record's history at once, submit makes the draft a version through the rules propose is
judged by and writes no section again. Only the author writes; take-over hands a draft, a proposal or a reserved number
to the caller with the reason in the next version's message. A new version of an effort starts from the served sections
and keeps its title, date and deciders (the rows of the commit folded in, now run over the ledger too). A tool call is
named to the ledger so the runtime stamps the caller it proved.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:52:53 +01:00
jschoubben 4aa77a4237 nats dump keeps and says the snapshot's exit status; restic blots JSON credentials and cuts on a character
mesh/delivery merged: source.main moved to novox/mesh-catalog@7fe569ef4067
mesh/check being carried to 4aa77a4237dd on 7fe569ef4067 | from 7f626147be0c
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:50:29 +01:00
jschoubben 4d95d8fc1e CAT-V2: a person's rotate of a machine's ledgers secret is refused
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:49:48 +01:00
jschoubben d6c3fc1e40 CAT-C9: mesh-config's first whole-module integration test, the rename window and the retirement against a real bus
The built binary, played as the node's tool runtime over a real bus: a renamed value moves and both
names answer, the module restarted inside the window keeps it and says renamed once, a refused move
writes its finding, g14's window closes on delivered, a value no manifest reads is asked about once
when no node reads it and removed only on the Approve.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:48:51 +01:00
jschoubben 285099a940 mesh-knowledge: a research effort's new version may leave out its sections
Closing a long research effort (its status and became links) meant retyping
its whole text through propose. A research effort's new version that gives no
sections now keeps the served version's, and its title, date and deciders when
none are given; the version so composed is judged and reviewed as any other.
A decision, or an effort with no served version, still needs its text.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:48:17 +01:00
jschoubben 80a7cee6f3 CAT-K3: each operation of the records' ledger types has its effect on a head, tested against the manifest's own types
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:47:59 +01:00
jschoubben e0d7644bb6 CAT-K2: a Store on the ledger, the records' heads without text and each section's text once
ledgerStore implements Store on the tool runner's ledger calls: a record is the instance decision/0290 or research/081,
a Save is the operation named by what changed, a section's text is written once in its write-section and named by
digest and sequence in the head, a version is read back from the history, the whole-set findings and the told marks
are keys of the state findings. Every test that runs over each store now also runs over the ledger, played with the
manifest's own types.

The bundle's pipe to the tool runner needs the SDK's raw request (request to A1); until then it answers loudly.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:47:31 +01:00
jschoubben 376a8b0fb5 CAT-C1..C8, C10: mesh-config moves a renamed config value, answers both names in the rename window, retires an unread value on the operator's Approve; the keeper reads its findings
A declaration's renamed-from moves the value with its trust and opens a window per node,
closed on delivered and, for a returning node-engine, on manifest-reads-changed (ADR 0618).
A value no manifest reads is marked retiring only once no node runs a build that reads it,
asked once, removed only on the Approve (a secret through the seat mesh-vault's retire).
New state in the old sections (ADR 0619), the stream config-events, the tools config_renames,
config_rename_again, config_rename_close, config_retiring and the seat verb renames.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:46:16 +01:00
jschoubben 6a39f43404 restic: a failed command reports the tail of its output, not only its exit status
Issue 1585: the backup before a nats move failed with only 'sh: exit status 1'.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:46:12 +01:00
jschoubben c973b31820 CAT-K1: mesh-knowledge asks for its records as a ledger, its states and its reviews stream, and says the new events
The manifest keeps postgres-database until the drop and asks, in the typed form, the ledger records (a type for each
kind, the operations of the move), the states drafts, asked, findings and sent, and the stream reviews (kept for good,
refusing when full, granted by the trusted value readers). It consumes claude-code's judged and says the eleven new
events. The mesh-delivery indexer's judge accepts it (checked against main).

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:39:47 +01:00
mesh-tools 7f626147be Merge pull request 'nats removes a push consumer; gitea, mesh-conditions, mesh-issues and messenger drop what named the removed controller (issue 1126)' (#607) from fix/consumer-remove-any into main 2026-11-04 10:36:41 +00:00
jschoubben 080aed6855 gitea: the merge refusal no longer says how long the last check took; nothing writes that record (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-catalog@7f626147be0c
mesh/delivery-group group fix/consumer-remove-any merged: every member is merged
mesh/check being carried to 080aed68556a on 7f626147be0c | from 7a7db0166ad2, mesh-delivery dd90a26251b9
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:06:48 +01:00
jschoubben 4f67143a78 gitea: remove what only tests called: the gate writer's verdict and its ask and history weighing, the late-verdict comment, the check-duration writer, the sibling record's two writers (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:05:50 +01:00
jschoubben 1572ab9b25 messenger: remove the old controller's walk decoder and its plan-moved names; the view's events are the only walks (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:02:56 +01:00
jschoubben b0b7dfc5eb mesh-issues: the removed controller is no registered owner and no core owner; old issues keep what they name (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:02:12 +01:00
jschoubben bf4ce7ef7b mesh-conditions: remove the hold's controller special case, bound to a module that is no more (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:01:11 +01:00
jschoubben 052fb473d0 gitea: remove the code bound to no event since ADR 0571 clause 13: the verdict setter, the controller's checked and plan types, the build seat's started (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 11:00:53 +01:00
jschoubben bff850f798 nats: nats_consumer_remove reads a push consumer too (issue 1126)
js.Consumer refuses a push consumer ("consumer is not a pull consumer"), so the
controller's leftovers on EVENTS and CONTROL could not be read and were not
removed. The tool now reads a consumer of either kind (CONSUMER.INFO only, which
is all its grant gives), before the delete and for the confirmation after it.

Quick fix: this change, in the tool, one function, about an hour with its test
rows (a pull and a push consumer). Thorough fix: the same tool also needs its
grant to cover the KV_* streams' consumers (mesh-controller-take-in,
mesh-controller-config-follow), and that grant is composed by mesh-sdk's
BusRemovalGrants from a closed set (ControllerLeftoverConsumersOn), not by the
manifest, so it is not in this commit: it needs a change in mesh-sdk and a
group delivery. Chosen here: the quick fix for the read, since it is one place
and blocks the operator's removal now; the grant is reported to the lead.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 10:45:00 +01:00
511 changed files with 22814 additions and 4313 deletions
+2
View File
@@ -7,6 +7,8 @@ modules/slack/cmd/slack-tools/slack-tools
modules/jetbrains-toolbox/cmd/toolbox-tools/toolbox-tools
modules/messenger/cmd/messenger/messenger
modules/mesh-watcher/cmd/mesh-watcher/mesh-watcher
modules/mesh-conditions/mesh-conditions
modules/minio/object-store
modules/claude-code/cmd/claude-code/claude-code
modules/dnsmasq/cmd/dnsmasq-tools/dnsmasq-tools
# ...and the same built from the module's root (go build ./cmd/<name>), which names it after the module.
+3 -1
View File
@@ -2,4 +2,6 @@ module checks/credentialform
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
+4 -2
View File
@@ -1,2 +1,4 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
+124
View File
@@ -0,0 +1,124 @@
// Package gitignored refuses an ignore pattern that hides a module's source (catalogue review F1, and its kind): the
// bare pattern `mesh-build-node` in a module's .gitignore, meant for the binary `go build` leaves, also matched the
// directory cmd/mesh-build-node, so git ignored every new file there and publish.go was never committed. A path under
// a module's cmd/ or internal/ is source: no .gitignore, the module's or the catalogue's, may ignore one, tracked or
// new. A binary is ignored by an anchored pattern (`/mesh-build-node`, `/cmd/mesh-build-node/mesh-build-node`).
package gitignored
import (
"bytes"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// Probe is the file name asked of each source directory: a Go file a change might add there.
const Probe = "added_by_a_change.go"
// Hidden is one source path an ignore pattern hides, and the pattern (its file, line and text, as git names it).
type Hidden struct {
Path, Pattern string
}
// Sources are the source directories of root's modules, relative to root: every directory under modules/*/cmd and
// modules/*/internal (node_modules left out).
func Sources(root string) ([]string, error) {
mods, err := filepath.Glob(filepath.Join(root, "modules", "*"))
if err != nil {
return nil, err
}
var out []string
for _, m := range mods {
for _, top := range []string{"cmd", "internal"} {
base := filepath.Join(m, top)
if st, err := os.Stat(base); err != nil || !st.IsDir() {
continue
}
err := filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
return nil
}
if d.Name() == "node_modules" {
return filepath.SkipDir
}
rel, _ := filepath.Rel(root, p)
out = append(out, filepath.ToSlash(rel))
return nil
})
if err != nil {
return nil, err
}
}
}
sort.Strings(out)
return out, nil
}
// isSource is whether a file is source a module's build reads: Go and TypeScript files and a Go module's own files.
// What a build or a test script leaves beside them (a binary, compiled JavaScript) may be ignored.
func isSource(name string) bool {
switch filepath.Ext(name) {
case ".go", ".ts", ".tsx":
return true
}
return name == "go.mod" || name == "go.sum"
}
// Find asks git which of the source paths root's ignore files hide: each source directory, a Go file added in it, and
// every source file there (isSource). It reads the .gitignore files of root's tree through a repository of its own, so it judges a git
// checkout and a plain folder alike, and no one's global ignores count.
func Find(root string, dirs []string) ([]Hidden, error) {
gitDir, err := os.MkdirTemp("", "gitignored-")
if err != nil {
return nil, err
}
defer os.RemoveAll(gitDir)
if out, err := exec.Command("git", "init", "-q", "--bare", gitDir).CombinedOutput(); err != nil {
return nil, fmt.Errorf("git init: %v: %s", err, out)
}
var ask bytes.Buffer
for _, d := range dirs {
ask.WriteString(d + "/\x00" + d + "/" + Probe + "\x00")
entries, err := os.ReadDir(filepath.Join(root, d))
if err != nil {
return nil, err
}
for _, e := range entries {
if !e.IsDir() && isSource(e.Name()) {
ask.WriteString(d + "/" + e.Name() + "\x00")
}
}
}
abs, err := filepath.Abs(root)
if err != nil {
return nil, err
}
cmd := exec.Command("git", "--git-dir="+gitDir, "--work-tree="+abs, "-c", "core.excludesFile=/dev/null",
"check-ignore", "--no-index", "-v", "-z", "--stdin")
cmd.Dir = abs
cmd.Stdin = &ask
out, err := cmd.Output()
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
return nil, nil // nothing is ignored
}
if err != nil {
return nil, fmt.Errorf("git check-ignore: %w", err)
}
// -v -z answers source, line, pattern and path, each ended by NUL.
f := strings.Split(strings.TrimRight(string(out), "\x00"), "\x00")
var hidden []Hidden
for i := 0; i+3 < len(f); i += 4 {
if f[i+2] == "" || strings.HasPrefix(f[i+2], "!") {
continue // a negation keeps the path
}
hidden = append(hidden, Hidden{Path: f[i+3], Pattern: fmt.Sprintf("%s:%s: %s", f[i], f[i+1], f[i+2])})
}
return hidden, nil
}
+80
View File
@@ -0,0 +1,80 @@
package gitignored
import (
"os"
"path/filepath"
"strings"
"testing"
)
// The kind of catalogue review F1: a bare pattern hides the command directory it shares a name with; an anchored one
// hides the binary alone; a glob that ignores source is refused too; a negation keeps a path.
func TestFindNamesEachPatternThatHidesSource(t *testing.T) {
for name, c := range map[string]struct {
ignore string
hidden []string
}{
"a bare name hides cmd/<name>": {"nfs-server\n", []string{"modules/m/cmd/nfs-server/", "modules/m/cmd/nfs-server/" + Probe, "modules/m/cmd/nfs-server/main.go"}},
"anchored to the binary": {"/nfs-server\n/cmd/nfs-server/nfs-server\n", nil},
"a glob ignoring Go source": {"*.go\n!main.go\n", []string{"modules/m/cmd/" + Probe, "modules/m/cmd/nfs-server/" + Probe,
"modules/m/internal/" + Probe, "modules/m/internal/x/" + Probe, "modules/m/internal/x/x.go"}},
"a pattern over internal/": {"internal/\n", []string{"modules/m/internal/", "modules/m/internal/" + Probe, "modules/m/internal/x/",
"modules/m/internal/x/" + Probe, "modules/m/internal/x/x.go"}},
"nothing but generated JavaScript": {"*.js\n", nil},
} {
root := t.TempDir()
for f, b := range map[string]string{
"modules/m/.gitignore": c.ignore,
"modules/m/cmd/nfs-server/main.go": "package main\n",
"modules/m/cmd/nfs-server/run.js": "",
"modules/m/internal/x/x.go": "package x\n",
"modules/m/node_modules/y/index.go": "",
"modules/other/cmd/other/other.go": "package main\n",
} {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(b), 0o644); err != nil {
t.Fatal(err)
}
}
dirs, err := Sources(root)
if err != nil {
t.Fatal(err)
}
got, err := Find(root, dirs)
if err != nil {
t.Fatal(err)
}
var paths []string
for _, h := range got {
paths = append(paths, h.Path)
if !strings.HasPrefix(h.Pattern, "modules/m/.gitignore:") {
t.Errorf("%s: %s is named hidden by %q, want the module's .gitignore", name, h.Path, h.Pattern)
}
}
if strings.Join(paths, ",") != strings.Join(c.hidden, ",") {
t.Errorf("%s: hidden %v, want %v", name, paths, c.hidden)
}
}
}
// The catalogue's own tree: no ignore pattern hides a path under a module's cmd/ or internal/.
func TestNoIgnorePatternHidesAModulesSource(t *testing.T) {
root := filepath.Join("..", "..")
dirs, err := Sources(root)
if err != nil {
t.Fatal(err)
}
if len(dirs) == 0 {
t.Fatal("no module source directory found: the check reads nothing")
}
hidden, err := Find(root, dirs)
if err != nil {
t.Fatal(err)
}
for _, h := range hidden {
t.Errorf("%s is hidden by %s: a module's source is never ignored; anchor the pattern to the binary (/<name>, /cmd/<name>/<name>)", h.Path, h.Pattern)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/gitignored
go 1.22
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/gofmt
go 1.22
+58
View File
@@ -0,0 +1,58 @@
// Package gofmt refuses a Go file of the catalogue that gofmt would change (the second of a kind in one milestone:
// buildstep's generated.go and claude-code's config_test.go reached a join unformatted). A check here holds every
// module to it, where a module's own tests hold only themselves.
package gofmt
import (
"bytes"
"go/format"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
// Files are the Go files under root, relative to it: every .go file but those in .git, node_modules and a testdata
// directory (a test's fixture may be anything).
func Files(root string) ([]string, error) {
var out []string
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case ".git", "node_modules", "testdata":
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(d.Name(), ".go") && d.Type().IsRegular() {
rel, _ := filepath.Rel(root, p)
out = append(out, filepath.ToSlash(rel))
}
return nil
})
sort.Strings(out)
return out, err
}
// Unformatted are the files gofmt would change, and why for one that does not parse.
func Unformatted(root string, files []string) (map[string]string, error) {
out := map[string]string{}
for _, f := range files {
src, err := os.ReadFile(filepath.Join(root, f))
if err != nil {
return nil, err
}
got, err := format.Source(src)
switch {
case err != nil:
out[f] = "it does not parse: " + err.Error()
case !bytes.Equal(got, src):
out[f] = "gofmt changes it: run gofmt -w " + f
}
}
return out, nil
}
+53
View File
@@ -0,0 +1,53 @@
package gofmt
import (
"os"
"path/filepath"
"testing"
)
func TestUnformattedNamesWhatGofmtChanges(t *testing.T) {
root := t.TempDir()
for f, src := range map[string]string{
"modules/a/a.go": "package a\n",
"modules/b/b.go": "package b\nimport (\n\t\"os\"\n\t\"fmt\"\n)\nvar _, _ = fmt.Sprint, os.Exit\n",
"modules/c/testdata/c.go": "package c",
"modules/d/node_modules/d.go": "package d",
} {
p := filepath.Join(root, f)
_ = os.MkdirAll(filepath.Dir(p), 0o755)
if err := os.WriteFile(p, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
}
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
got, err := Unformatted(root, files)
if err != nil {
t.Fatal(err)
}
if len(files) != 2 || len(got) != 1 || got["modules/b/b.go"] == "" {
t.Errorf("files %v, unformatted %v; want a.go and b.go read, b.go named", files, got)
}
}
// The catalogue's own Go files are as gofmt writes them.
func TestTheCataloguesGoIsFormatted(t *testing.T) {
root := filepath.Join("..", "..")
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
if len(files) == 0 {
t.Fatal("no Go file found: the check reads nothing")
}
got, err := Unformatted(root, files)
if err != nil {
t.Fatal(err)
}
for f, why := range got {
t.Errorf("%s: %s", f, why)
}
}
+3 -1
View File
@@ -2,4 +2,6 @@ module checks/health
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
+4 -2
View File
@@ -1,2 +1,4 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
+3 -1
View File
@@ -2,4 +2,6 @@ module checks/manifests
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
+4 -2
View File
@@ -1,2 +1,4 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
+54 -2
View File
@@ -1,5 +1,5 @@
// 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.
// read: a module.json that does not decode, a module.json giving one field twice, and two manifests that name one module.
//
// 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
@@ -9,6 +9,8 @@
package manifests
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -18,7 +20,9 @@ import (
)
// 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.
// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), one giving a field twice (a decoder keeps the
// last and drops the first without a word: a merge of two branches that each added the field writes it so), 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 {
@@ -38,6 +42,10 @@ func Problems(root string) ([]string, error) {
out = append(out, fmt.Sprintf("%s: %v", rel, err))
continue
}
if twice := givenTwice(raw); twice != "" {
out = append(out, fmt.Sprintf("%s: the field %s is given twice; a judge reads only the last", rel, twice))
continue
}
if named[m.Module] {
out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module))
continue
@@ -46,3 +54,47 @@ func Problems(root string) ([]string, error) {
}
return out, nil
}
// givenTwice is the first field an object of raw gives twice, as its path (`invokes`, `build.artifacts[0].env`), or empty.
func givenTwice(raw []byte) string {
d := json.NewDecoder(bytes.NewReader(raw))
var walk func(path string) string
walk = func(path string) string {
t, err := d.Token()
if err != nil {
return ""
}
switch t {
case json.Delim('{'):
seen := map[string]bool{}
for d.More() {
k, err := d.Token()
if err != nil {
return ""
}
key, _ := k.(string)
at := key
if path != "" {
at = path + "." + key
}
if seen[key] {
return at
}
seen[key] = true
if twice := walk(at); twice != "" {
return twice
}
}
_, _ = d.Token()
case json.Delim('['):
for i := 0; d.More(); i++ {
if twice := walk(fmt.Sprintf("%s[%d]", path, i)); twice != "" {
return twice
}
}
_, _ = d.Token()
}
return ""
}
return walk("")
}
+5 -1
View File
@@ -10,7 +10,7 @@ import (
)
// 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
// manifest that is no JSON, one giving a field twice 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 {
@@ -21,6 +21,10 @@ func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) {
{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: "a field given twice", files: map[string]string{"a": `{"module":"a","version":"1","invokes":["x.y"],"build":{"artifacts":[{}]},"invokes":["z.w"]}`},
refused: "modules/a/module.json: the field invokes is given twice"},
{name: "a field given twice deeper", files: map[string]string{"a": `{"module":"a","version":"1","build":{"artifacts":[{"name":"t","name":"u"}]}}`},
refused: "the field build.artifacts[0].name is given twice"},
{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"},
} {
+3 -1
View File
@@ -2,4 +2,6 @@ module checks/readable
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
+4 -2
View File
@@ -1,2 +1,4 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
+1 -1
View File
@@ -1,4 +1,4 @@
58
57
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
+2 -2
View File
@@ -2,10 +2,10 @@ module adwaita
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
@@ -8,10 +8,13 @@ import (
"fmt"
"os"
"strings"
"sync/atomic"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
"artifactstoretools/internal/registry"
)
@@ -45,10 +48,22 @@ func DoorAt(file string) func() (Door, error) {
}
// runtimeBus is the holder's bus through the runtime: its reads, its findings and its seat's event.
type runtimeBus struct{}
type runtimeBus struct{ views *views.Reader }
func (runtimeBus) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func (runtimeBus) Get(state, key string) ([]byte, bool, error) {
// followed says whether the state is the indexer's or the assigner's, which the holder asks once and follows by events
// (ADR 0615) rather than reads.
func (b runtimeBus) followed(state string) bool { return b.views != nil && b.views.Follows(state) }
func (b runtimeBus) Keys(state string) ([]string, error) {
if b.followed(state) {
return b.views.Keys(state)
}
return stdio.State(state).Keys()
}
func (b runtimeBus) Get(state, key string) ([]byte, bool, error) {
if b.followed(state) {
return b.views.Get(state, key)
}
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
@@ -82,7 +97,7 @@ func (runtimeBus) Publish(subject string, body any) error {
// 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) {
func follow(h *Holder, v *views.Reader) {
<-stdio.Served()
watch := func(state string, handle func(stdio.StateChange) error) {
for {
@@ -111,11 +126,22 @@ func follow(h *Holder) {
}
return nil
})
watch(WantsRead, func(c stdio.StateChange) error {
if !c.Current {
go observe()
// The wants are the assigner's: the first view then its events (ADR 0615). What Watch hands over before it returns
// is the first view, not a change.
var current atomic.Bool
current.Store(true)
for {
err := v.Watch(WantsRead, func(string, []byte, bool) {
if !current.Load() {
go observe()
}
})
if err == nil {
break
}
return nil
})
h.logf("watching %s: %v; asked again in 10s", WantsRead, err)
time.Sleep(10 * time.Second)
}
current.Store(false)
observe()
}
@@ -29,6 +29,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/views"
"artifactstoretools/internal/registry"
)
@@ -903,6 +905,12 @@ func (h *Holder) Observe(ctx context.Context) error {
continue // the sweep's own finding, which the sweep clears
}
if _, still := now[k]; !still {
// A finding of a first view unanswered is the reader's (views.Reader), cleared by its answer, not by a sweep.
if raw, found, err := h.Bus.Get(FindingsState, k); err == nil && found {
if f, err := health.DecodeFinding(raw); err == nil && f.Kind == views.UnansweredKind {
continue
}
}
if err := h.Bus.Delete(FindingsState, k); err != nil {
return err
}
@@ -10,12 +10,16 @@
package main
import (
"context"
"fmt"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
@@ -26,13 +30,23 @@ func main() {
Run: ExecRunner,
UID: os.Getuid(),
}
h := &Holder{Bus: runtimeBus{}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
v := &views.Reader{Module: "artifact-store-tools", Logf: logf, Findings: views.OwnFindings{}, Acts: handacts.Recorder{Ask: stdio.Ask}}
v.Follow(delivery.AssignerModule, "wants", WantsTool)
v.Follow(delivery.IndexerModule, "manifests", ManifestsTool)
h := &Holder{Bus: runtimeBus{views: v}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
Seats: stdio.Ask, Now: time.Now, Logf: logf}
h.Acts = handacts.Recorder{Ask: stdio.Ask}
go follow(h)
v.Start(context.Background())
go follow(h, v)
// 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 {
if err := stdio.Serve("", append(append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...), v.Tools("artifact_store")...)); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// The owners' read tools of the states the holder follows, which the manifest invokes (ADR 0615).
const (
WantsTool = delivery.AssignerModule + ".assigner_read_wants"
ManifestsTool = delivery.IndexerModule + ".indexer_read_manifests"
)
@@ -2,12 +2,14 @@ package main
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/views"
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"slices"
"sort"
"strconv"
"strings"
@@ -443,10 +445,11 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Fatal(err)
}
var m struct {
Tools []string `json:"tools"`
Invokes []string `json:"invokes"`
Reads []string `json:"reads"`
Claims []struct {
Tools []string `json:"tools"`
Invokes []string `json:"invokes"`
Reads []string `json:"reads"`
Consumes []string `json:"consumes"`
Claims []struct {
Name string `json:"name"`
Serves []string `json:"serves"`
} `json:"claims"`
@@ -455,7 +458,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Fatal(err)
}
var served []string
for _, tl := range Tools(&Store{}, recordsAsked{&asked{}}, &Catalogue{}) {
for _, tl := range append(Tools(&Store{}, recordsAsked{&asked{}}, &Catalogue{}), (&views.Reader{}).Tools("artifact_store")...) {
if !strings.HasPrefix(tl.Name, "artifact_store_") || tl.Description == "" || tl.Run == nil {
t.Errorf("tool %q", tl.Name)
}
@@ -470,12 +473,17 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
// ADR 0571 clause 2: it holds the seat mesh-artifact-sweep and serves its verbs, and calls no verb of the controller's;
// it reads no controller state, the lease neither (issue 1126).
// CAT-AS1: what it holds and removes it asks the two registry seats the artifact store's holder serves.
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule,
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule, WantsTool, ManifestsTool,
"seat:oci-artifact-registry.hold", "seat:oci-artifact-registry.release", "seat:oci-artifact-registry.remove",
"seat:container-image-registry.hold", "seat:container-image-registry.release", "seat:container-image-registry.remove"}
if !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes %v", m.Invokes)
}
// ADR 0615: the assigner's wants and the indexer's manifests are asked once and followed by their events, not read.
if !reflect.DeepEqual(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.views.manifests"}) ||
slices.Contains(m.Reads, WantsRead) || slices.Contains(m.Reads, ManifestsRead) {
t.Errorf("consumes %v, reads %v", m.Consumes, m.Reads)
}
var verbs []string
for _, tl := range SeatTools(&Holder{}, &Store{}) {
verbs = append(verbs, strings.TrimPrefix(tl.Name, catalogue.ArtifactSweepSeat+"."))
+9 -7
View File
@@ -3,22 +3,24 @@ module artifactstoretools
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats.go v1.54.0
)
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
require (
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // indirect
github.com/nats-io/nats-server/v2 v2.11.17 // 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
golang.org/x/time v0.15.0 // indirect
golang.org/x/time v0.16.0 // indirect
)
+16 -10
View File
@@ -1,15 +1,21 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/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=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639 h1:5HeZzk6tKzcSwVivD3IcIvQ/2WIvysZvNvNKyq/oIBA=
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639 h1:+bkN8op2yrAlBYrOB+hYnIoIZkbjXsNmc/4//uMnL4E=
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
@@ -23,5 +29,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
+9 -3
View File
@@ -21,6 +21,8 @@
"seat:mesh-hand-acts.hand-act",
"seat:mesh-catalog.modules",
"seat:mesh-catalog.module",
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-indexer.indexer_read_manifests",
"seat:oci-artifact-registry.hold",
"seat:oci-artifact-registry.release",
"seat:oci-artifact-registry.remove",
@@ -29,12 +31,14 @@
"seat:container-image-registry.remove"
],
"reads": [
"mesh-delivery-assigner.wants",
"mesh-delivery-builder.builds",
"mesh-delivery-builder.build-log",
"mesh-delivery-indexer.manifests",
"node-engine.have"
],
"consumes": [
"mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.manifests"
],
"state": [
"findings"
],
@@ -42,7 +46,9 @@
"artifact_store_repositories",
"artifact_store_usage",
"artifact_store_references",
"artifact_store_collect"
"artifact_store_collect",
"artifact_store_first_views",
"artifact_store_first_view_again"
],
"config": {
"older-store-names": {
+2 -2
View File
@@ -2,10 +2,10 @@ module asuszephyrusg14
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module avahi
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module blueman
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module bluetooth
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+6 -5
View File
@@ -3,20 +3,21 @@ module brother
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // 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
golang.org/x/time v0.15.0 // indirect
golang.org/x/time v0.16.0 // indirect
)
+12 -10
View File
@@ -1,15 +1,17 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
@@ -23,5 +25,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
@@ -269,12 +269,18 @@ func TestTheManifestListsEveryToolServed(t *testing.T) {
t.Fatal(err)
}
var m struct {
Tools []string `json:"tools"`
Invokes []string `json:"invokes"`
Tools []string `json:"tools"`
Invokes []string `json:"invokes"`
Reads []string `json:"reads"`
Consumes []string `json:"consumes"`
}
_ = 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)
// The hand-act seat's warranted and hand-act, and the owners' read tools of the three followed states; no verb of
// the controller, and no read of the indexer's or the assigner's state (ADR 0615 §1): their views are consumed.
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.hand-act", ModulesTool, ManifestsTool, PlacementTool}) ||
!slices.Equal(m.Reads, []string{"claude-licence-manager.bindings"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests", "mesh-delivery-assigner.views.placement"}) {
t.Errorf("invokes = %v, reads = %v, consumes = %v", m.Invokes, m.Reads, m.Consumes)
}
if src, _ := os.ReadFile("proposals.go"); !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) ||
strings.Contains(string(src), "seat:mesh-controller.warranted") {
@@ -285,6 +291,7 @@ func TestTheManifestListsEveryToolServed(t *testing.T) {
listed[n] = true
}
p, _ := node(t, "laptop")
meshViews = newMeshViews()
served := tools(p, nil, NewServerView(p), memConfig{}, NewConfigView(p))
for _, tool := range served {
if !listed[tool.Name] {
+7 -6
View File
@@ -25,7 +25,8 @@ import (
"path/filepath"
"strings"
"git.novox.be/novox/mesh-sdk/go/asks"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
)
// ForgeProvision is the provision whose credential this is, and the file this module's manifest has it delivered to.
@@ -55,16 +56,16 @@ func (f *ForgeHealth) key() string { return "claude-code.machine." + f.Node + ".
// Look is the condition to say after one placing of the credential, nil when nothing changed: raised on a failure, again
// when its reason changes, cleared when it places again; on the first look a success clears what a run before raised.
func (f *ForgeHealth) Look(err error) *asks.Condition {
func (f *ForgeHealth) Look(err error) *oc.Condition {
first := !f.looked
f.looked = true
subject := asks.Scope{Scope: "machine", ID: f.Node, Machine: f.Node}
subject := oc.ConditionSubject{Scope: "machine", ID: f.Node, Machine: f.Node}
if err == nil {
if f.raised == "" && !first {
return nil
}
f.raised = ""
return &asks.Condition{Key: f.key(), Subject: subject, State: asks.StateCleared}
return &oc.Condition{Key: f.key(), Subject: subject, State: occ.StateCleared}
}
why := err.Error()
if why == f.raised {
@@ -72,10 +73,10 @@ func (f *ForgeHealth) Look(err error) *asks.Condition {
}
f.raised = why
needs := "read claude-code's journal on " + f.Node + " and the owner of its forge credential file"
return &asks.Condition{Key: f.key(), Severity: asks.SeverityWarning, State: asks.StateRaised, Subject: subject,
return &oc.Condition{Key: f.key(), Severity: occ.SeverityWarning, State: occ.StateRaised, Subject: subject,
Headline: "Agents on " + f.Node + " cannot use the forge credential",
Summary: why, Needs: needs,
Explanation: asks.NeedsYou + needs + ". " + sentence(why) + " Until it is placed, an agent there cannot " +
Explanation: occ.NeedsYou + needs + ". " + sentence(why) + " Until it is placed, an agent there cannot " +
"clone or fetch the mesh's private repositories.",
Resolved: "The forge's read credential is placed again on " + f.Node + "."}
}
@@ -12,7 +12,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/asks"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
)
const agentToken = "0123456789abcdef0123456789abcdef01234567"
@@ -316,7 +316,7 @@ func TestTheForgeCredentialIsTheRuntimesAndItsFailureIsAConditionOfTheModule(t *
got := ""
if c != nil {
got = c.State
if _, err := asks.EncodeCondition(*c); err != nil {
if _, err := occ.EncodeCondition(*c); err != nil {
t.Errorf("look %d: the condition is refused: %v", i, err)
}
if c.Key != "claude-code.machine.anchor.forge-credential" {
@@ -9,25 +9,25 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-contracts/go/seats"
)
// meshForTheGuard is a mesh of two machines and the verbs that replace what an agent runs over ssh, as the
// seats' contract and the modules' manifests say them.
func meshForTheGuard() GuardData {
seats := []catalogue.Seat{
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
defined := []seats.Seat{
{Name: "mesh-placement", Scope: "mesh", Serves: []seats.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: "node-service-manager", Scope: "node", Serves: []seats.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: "node-hostname", Scope: "node", Serves: []seats.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{
return GuardData{Node: "laptop", Replacements: ReplacementsOf(defined, 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"}},
}}
@@ -1,16 +1,20 @@
package main
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-contracts/go/seats"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
)
// aBus is the states claude-code reads, as a test holds them: by read, key and value.
@@ -33,12 +37,12 @@ func (b aBus) Get(read, key string) ([]byte, bool, error) {
}
// 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{
func someSeats() []seats.Seat {
return []seats.Seat{
{Name: "mesh-placement", Scope: "mesh", Serves: []seats.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: "node-service-manager", Scope: "node", Serves: []seats.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"}}}},
}
@@ -192,10 +196,44 @@ func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
}
}
// ADR 0615 §1: the tools and the placement come from the owners' first views and their views events, never their state.
// A reader whose first views are answered from the fixture reads what the state read gave, and a views event changes
// what it reads (CAT-CC3).
func TestTheMeshIsReadFromTheOwnersFirstViews(t *testing.T) {
bus := aMesh(t)
r := newMeshViews()
for _, name := range []string{ModulesRead, ManifestsRead, PlacementRead} {
state := strings.TrimPrefix(name, strings.Split(name, ".")[0]+".")
values := map[string]json.RawMessage{}
for k, v := range bus[name] {
values[k] = v
}
f := r.Follow(strings.Split(name, ".")[0], state, "x")
f.Ask = func(context.Context, string) (views.Answer, error) {
return views.Answer{V: views.Version, Owner: f.Owner, State: state, Revision: 5, Created: time.Unix(1, 0), Values: values, At: time.Now()}, nil
}
if err := f.Start(context.Background()); err != nil {
t.Fatal(err)
}
}
want, err := ReadMeshToolsFrom(bus, seats.List(), resolving)
if err != nil {
t.Fatal(err)
}
got, err := ReadMeshToolsFrom(viewStates{r}, seats.List(), resolving)
if err != nil || !reflect.DeepEqual(got, want) || len(got.Replacements) == 0 {
t.Fatalf("from the views: %+v, %v\nfrom the state: %+v", got, err, want)
}
// A reader not yet answered says so, and reads nothing.
if _, err := ReadMeshToolsFrom(viewStates{newMeshViews()}, seats.List(), resolving); err == nil || !strings.Contains(err.Error(), "no first view") {
t.Fatalf("an unanswered first view: %v", err)
}
}
// 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)
m, err := ReadMeshToolsFrom(aMesh(t), seats.List(), resolving)
if err != nil {
t.Fatal(err)
}
@@ -71,6 +71,17 @@ type Judged struct {
Pending bool `json:"pending,omitempty"`
}
// JudgedEvent is the body of `judged`: one per judgement asked with a bound past JudgeBound, said when it ends, with
// its answer or why there is none. The question and the schema are never in it.
type JudgedEvent struct {
Ticket string `json:"ticket"`
Node string `json:"node"`
Model string `json:"model,omitempty"`
Seconds float64 `json:"seconds,omitempty"`
Answer json.RawMessage `json:"answer,omitempty"`
Error string `json:"error,omitempty"`
}
// running is one judgement asked with a bound past JudgeBound, until it is collected.
type running struct {
done chan struct{}
@@ -92,6 +103,9 @@ type Judge struct {
// Wait is how long a call waits for a long judgement before it answers its ticket; JudgeWait unless a test
// says less.
Wait time.Duration
// Say tells the bus a ticketed judgement ended: the event `judged`, once, under the message id it is given so a
// repeat is dropped. Nil says nothing (a test's judge); the ticket is collected by claude_code_judged all the same.
Say func(body JudgedEvent, id string) error
turn chan struct{}
mu sync.Mutex
@@ -208,10 +222,28 @@ func (j *Judge) start(schema, model, question string, bound time.Duration) (*Jud
go func() {
r.out, r.err = j.run(schema, model, question, bound)
close(r.done)
j.said(ticket, r)
}()
return j.Collect(ticket)
}
// said tells the bus that the judgement under a ticket ended (the event `judged`), once. A failure to say is logged: the
// ticket stays collectible by claude_code_judged, and the asker's own bound ends its wait.
func (j *Judge) said(ticket string, r *running) {
if j.Say == nil {
return
}
e := JudgedEvent{Ticket: ticket, Node: j.P.Node}
if r.err != nil {
e.Error = r.err.Error()
} else {
e.Model, e.Seconds, e.Answer = r.out.Model, r.out.Seconds, r.out.Answer
}
if err := j.Say(e, "judged-"+j.P.Node+"-"+ticket); err != nil {
say("the judgement under ticket %s ended and could not be said on the bus: %v", ticket, err)
}
}
// Collect waits as long as a call may for the judgement under a ticket: its answer, why it failed, or that it is
// still pending. A ticket is answered once; an answer nobody collects is kept JudgedKept past its bound.
func (j *Judge) Collect(ticket string) (*Judged, error) {
@@ -182,6 +182,9 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
}
})
j.Wait = 50 * time.Millisecond
said := make(chan JudgedEvent, 4)
ids := make(chan string, 4)
j.Say = func(e JudgedEvent, id string) error { said <- e; ids <- id; return nil }
start := time.Now()
out, err := j.Judge(JudgeAsk{Question: "a long record", Schema: verdictSchema, Within: 90 * time.Second})
if err != nil {
@@ -203,6 +206,21 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
if got.Pending || string(got.Answer) != `{"verdict":"none"}` || got.Ticket != out.Ticket {
t.Fatalf("%+v", got)
}
// One `judged` per ticket, with the answer, under an id of the ticket's own; none for a judgement with no ticket.
select {
case e := <-said:
if e.Ticket != out.Ticket || e.Node != "control" || string(e.Answer) != `{"verdict":"none"}` || e.Error != "" || e.Model != "sonnet" {
t.Fatalf("judged: %+v", e)
}
if id := <-ids; id != "judged-control-"+out.Ticket {
t.Fatalf("judged under %q", id)
}
case <-time.After(5 * time.Second):
t.Fatal("the ended judgement was not said")
}
if len(said) != 0 {
t.Fatal("judged said twice")
}
// Read once the judgement is collected: its end is what orders the agent's write before this read.
if left := deadline.Sub(start); left < 80*time.Second || left > 91*time.Second {
t.Fatalf("the judgement's bound is %s, not the 90s asked", left)
@@ -224,10 +242,23 @@ func TestALongJudgementIsBoundedAndSaysWhyItFailed(t *testing.T) {
}
j.Wait = 20 * 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
failed := make(chan JudgedEvent, 1)
j.Say = func(e JudgedEvent, _ string) error { failed <- e; return nil }
out, err := j.Judge(JudgeAsk{Question: "q", Schema: verdictSchema, Within: 2 * time.Second})
if err != nil || !out.Pending {
t.Fatalf("%+v %v", out, err)
}
defer func() {
// A failed judgement is said too, with why and no answer.
select {
case e := <-failed:
if e.Ticket != out.Ticket || e.Answer != nil || !strings.Contains(e.Error, "did not answer within") {
t.Errorf("judged: %+v", e)
}
case <-time.After(5 * time.Second):
t.Error("the failed judgement was not said")
}
}()
for i := 0; i < 3000; i++ {
got, err := j.Collect(out.Ticket)
if err != nil {
+53 -18
View File
@@ -12,6 +12,7 @@ package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -21,9 +22,13 @@ import (
"strings"
"time"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
"git.novox.be/novox/mesh-contracts/go/seats"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
)
func say(format string, args ...any) {
@@ -180,12 +185,12 @@ func (s memoryStateOf) Delete(key string) error { return s.s.Delete(key) }
var memoryCarrier *MemoryCarrier
// sayCondition says a condition on the operator-channel seat under this module's name (novox/hq ADR 0437 §9).
func sayCondition(c asks.Condition) error {
subject, err := asks.ConditionSubject("claude-code")
func sayCondition(c oc.Condition) error {
subject, err := occ.ConditionSubject("claude-code")
if err != nil {
return err
}
raw, err := asks.EncodeCondition(c)
raw, err := occ.EncodeCondition(c)
if err != nil {
return err
}
@@ -205,23 +210,46 @@ 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 seat mesh-placement's placement — for the register
// tool's question.
func nodesRunningMe() ([]string, error) { return NodesRunning(busStates{}, "claude-code") }
// nodesRunningMe is the nodes claude-code runs on, from the assigner's placement — for the register tool's question.
func nodesRunningMe() ([]string, error) { return NodesRunning(viewStates{meshViews}, "claude-code") }
// busStates reads the states the manifest declares in its reads through the runtime.
type busStates struct{}
// The owners' read tools of the states this module follows, which the manifest invokes (novox/hq ADR 0615 §1).
const (
ModulesTool = delivery.IndexerModule + ".indexer_read_modules"
ManifestsTool = delivery.IndexerModule + ".indexer_read_manifests"
PlacementTool = delivery.AssignerModule + ".assigner_read_placement"
)
func (busStates) Keys(read string) ([]string, error) { return stdio.State(read).Keys() }
// meshViews is this module's picture of the indexer's modules and manifests and the assigner's placement: each asked
// once of the owner's read tool, then followed on the owner's views family. It reads none of their state.
var meshViews *views.Reader
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
}
return e.Value, true, nil
// meshChanged is told, without waiting, that a followed state changed: the instructions' table is read again.
var meshChanged = make(chan struct{}, 1)
// newMeshViews builds the reader of those three states; Start asks them.
func newMeshViews() *views.Reader {
v := &views.Reader{Module: Module, Logf: func(f string, a ...any) { say(f, a...) }, Findings: views.OwnFindings{},
Acts: handacts.Recorder{Ask: stdio.Ask},
Changed: func(string, string, json.RawMessage, bool) {
select {
case meshChanged <- struct{}{}:
default:
}
}}
v.Follow(delivery.IndexerModule, "modules", ModulesTool)
v.Follow(delivery.IndexerModule, "manifests", ManifestsTool)
v.Follow(delivery.AssignerModule, "placement", PlacementTool)
return v
}
// viewStates reads the followed states from the reader's pictures, never from the owners' state.
type viewStates struct{ v *views.Reader }
func (s viewStates) Keys(read string) ([]string, error) { return s.v.Keys(read) }
func (s viewStates) Get(read, key string) ([]byte, bool, error) { return s.v.Get(read, key) }
func fingerprintOfFile(path string) any {
raw, err := os.ReadFile(path)
if err != nil {
@@ -291,7 +319,7 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
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 := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup); err != nil {
if m, err := ReadMeshToolsFrom(viewStates{meshViews}, seats.List(), lookup); err != nil {
answer["asked"] = "the bus could not be read in full (" + err.Error() + "); what was kept is rendered"
} else {
KeepMeshTools(p, m)
@@ -372,7 +400,12 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
out = append(out, proposalTools(p)...)
out = append(out, waitTools(p)...)
judge := NewJudge(p)
// The event `judged`, one per ticket when its judgement ends: the asker hears it instead of asking again.
judge.Say = func(e JudgedEvent, id string) error {
return stdio.Emit("judged", e, stdio.EmitOptions{MessageID: id})
}
out = append(out, judgeTool(judge), judgedTool(judge))
out = append(out, meshViews.Tools("claude_code")...)
return append(out, configTools(p, config, configView)...)
}
@@ -418,6 +451,7 @@ func main() {
return
}
p, launched := PathsFrom(os.Getenv)
meshViews = newMeshViews()
if launched {
fsys = FilesFor(p)
}
@@ -447,6 +481,7 @@ func main() {
}
}
}
meshViews.Start(context.Background())
go refreshMeshTools(p)
servers := stateOf{stdio.State("servers")}
view := NewServerView(p)
+30 -12
View File
@@ -5,7 +5,7 @@ package main
// replaces, and the mesh's machines by every name and address a session could reach one by.
//
// 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'
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-contracts), 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
@@ -23,7 +23,7 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-contracts/go/seats"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -217,14 +217,14 @@ func firstSentence(s string) string {
// 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 {
func ReplacementsOf(defined []seats.Seat, modules []ModuleTools) []Replacement {
var out []Replacement
for _, s := range seats {
for _, s := range defined {
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,
out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == "node", Seat: true,
Does: firstSentence(v.Description), Replaces: v.Replaces})
}
}
@@ -257,15 +257,15 @@ func lookup(name string) []string {
// 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) {
func ReadMeshToolsFrom(st States, defined []seats.Seat, resolve func(string) []string) (MeshTools, error) {
var m MeshTools
placed, err := placements(st)
if err != nil {
m.Replacements = ReplacementsOf(seats, nil)
m.Replacements = ReplacementsOf(defined, nil)
return m, err
}
modules, merr := modulesTools(st, runsOn(placed))
m.Replacements = ReplacementsOf(seats, modules)
m.Replacements = ReplacementsOf(defined, modules)
for node, p := range placed {
machine := Machine{Name: node, Domains: []string{node + InternalSuffix}}
if d := strings.ToLower(strings.TrimSpace(p.Decided.PublicDomain)); d != "" {
@@ -334,13 +334,23 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
}
// MeshToolsEvery is how often the bus is read again.
// MeshToolsEvery is how often the machines' addresses are resolved again; the tools and the placement are read again
// when their followed states change (a views event), not on this timer.
const MeshToolsEvery = 10 * time.Minute
// refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed.
// MeshToolsFirst is how long the first read waits for the first views to be answered.
const MeshToolsFirst = 2 * time.Minute
// refreshMeshTools reads the pictures once the first views are answered, and again whenever one changes (and the
// machines' addresses every few minutes), and renders when what it said changed.
func refreshMeshTools(p Paths) {
select {
case <-meshViews.Ready():
case <-time.After(MeshToolsFirst):
say("the first views of the tools and the placement were not answered in %s; reading what is held", MeshToolsFirst)
}
for {
m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup)
m, err := ReadMeshToolsFrom(viewStates{meshViews}, seats.List(), lookup)
if err != nil {
say("reading what each tool replaces and which machines the mesh has: %v", err)
}
@@ -351,7 +361,15 @@ func refreshMeshTools(p Paths) {
say("the mesh's tools changed: %d replace a command, %d machines; rendered", len(m.Replacements), len(m.Machines))
}
}
time.Sleep(MeshToolsEvery)
select {
case <-meshChanged:
time.Sleep(2 * time.Second) // a burst of events is one read
select {
case <-meshChanged:
default:
}
case <-time.After(MeshToolsEvery):
}
}
}
@@ -9,8 +9,8 @@ import (
"sync"
"time"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Proposing is what the proposal tools reach: the `proposals` state, the operator channel, the controller's
@@ -71,9 +71,13 @@ func (d *Proposing) watch(p Paths) {
}
}
go persist("hearing the operator's answers", func() error {
return stdio.Subscribe(asks.Seat+".decided."+Module, func(e stdio.Envelope) error {
var w asks.Warrant
if json.Unmarshal(e.Body, &w) == nil {
return stdio.Subscribe(oc.Seat+".decided."+Module, func(e stdio.Envelope) error {
var w oc.Warrant
if err := json.Unmarshal(e.Body, &w); err == nil {
if _, err := warrantAt(w); err != nil {
say("not taking the router's word on %s: %v", w.Ask, err)
return nil
}
if strings.HasPrefix(w.Ask, QuestionPrefix) {
HeardQuestionEnd(d.questions(), w)
return nil
@@ -15,7 +15,7 @@ package main
// and lets this module read only its own asks);
// 2. composes the ask again from the proposal — the words, the options, the expiry, the digest of the text —
// and holds the router's warrant to it: the same ask digest, an option it offered, at its level, in time
// (asks.Warrant.For), and the option's bound act is this proposal's (asks.Option.Performs). An ask published
// (oc.Warrant.For), and the option's bound act is this proposal's (oc.Option.Performs). An ask published
// under this module's name with other words, or a proposal changed after it was asked, finds the digests
// differ and nothing is done;
// 3. applies the approved text to its own file, `approved.json` in the module's state directory, which is what
@@ -42,8 +42,9 @@ import (
"unicode"
"unicode/utf8"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Module is this module's name: the one the bus grants its asks under.
@@ -68,7 +69,7 @@ const RouterKeeps = 30 * 24 * time.Hour
// ProposalLasts is how long the operator has to answer: a day less a margin, so the router never refuses it for
// lasting a day and a moment (the SDK's bound for an approval).
const ProposalLasts = asks.ApproveLasts - 10*time.Minute
const ProposalLasts = occ.ApproveLasts - 10*time.Minute
// What a proposal does.
const (
@@ -164,8 +165,8 @@ func joinAnd(list []string) string {
// Act is what an answer binds: this proposal, whole, and the answer. The router's warrant names the ask's digest,
// and the ask names this act's digest in the option, so an approval authorises this text and nothing else.
func (p Proposal) Act(option string) asks.Act {
return asks.Act{"verb": Module + ".instructions", "option": option, "proposal": p.ID, "action": p.Action,
func (p Proposal) Act(option string) occ.Act {
return occ.Act{"verb": Module + ".instructions", "option": option, "proposal": p.ID, "action": p.Action,
"scope": p.Scope, "nodes": strings.Join(p.Nodes, ","), "name": p.Name, "digest": p.Digest, "from": p.From,
"at": p.At.UTC().Format(time.RFC3339Nano)}
}
@@ -175,7 +176,7 @@ const AskGlance = 1
// Ask is the ask for a proposal, composed from it alone: every node composes the same one, and holds the
// router's warrant to it — in the form it was asked in.
func (p Proposal) Ask() asks.Ask {
func (p Proposal) Ask() oc.Ask {
if p.Form == AskGlance {
return p.glanceAsk()
}
@@ -185,7 +186,7 @@ func (p Proposal) Ask() asks.Ask {
// glanceAsk is the ask read at a glance on the phone (novox/hq issue 639): its headline, and as its explanation
// the summary of what it does; the text, where it came from, its fingerprint and where to read all of it are its
// Details, which the router notes after the answers, collapsed where the channel can.
func (p Proposal) glanceAsk() asks.Ask {
func (p Proposal) glanceAsk() oc.Ask {
verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action]
shown, whole := excerpt(p.Text)
part := ""
@@ -193,7 +194,7 @@ func (p Proposal) glanceAsk() asks.Ask {
part = " Part shown"
}
headline := fmt.Sprintf("%s Claude instructions '%s'?%s", verb, p.Name, part)
if len([]rune(headline)) > asks.HeadlineLength {
if len([]rune(headline)) > occ.HeadlineLength {
headline = verb + " Claude instructions?" + part
}
var d strings.Builder
@@ -214,7 +215,7 @@ func (p Proposal) glanceAsk() asks.Ask {
if p.Action == ActionRemove {
does = "the instructions are removed from those sessions"
}
return p.withOptions(asks.Ask{ID: p.ID, Headline: headline, Explanation: p.summary(), Details: d.String()}, does)
return p.withOptions(oc.Ask{ID: p.ID, Headline: headline, Explanation: p.summary(), Details: d.String()}, does)
}
// summary is what the proposal does, at a glance: its proposer's words, or the module's.
@@ -244,8 +245,8 @@ func SummaryProblem(summary string) string {
return ""
case strings.Contains(s, "\n"):
return "the summary is more than one line"
case len([]rune(s)) > asks.SummaryLength:
return fmt.Sprintf("the summary is %d characters, more than the %d a phone shows at a glance", len([]rune(s)), asks.SummaryLength)
case len([]rune(s)) > occ.SummaryLength:
return fmt.Sprintf("the summary is %d characters, more than the %d a phone shows at a glance", len([]rune(s)), occ.SummaryLength)
case !shownLine(s):
return "the summary names a path, an address, a host name or a long word, which a phone is not shown"
}
@@ -254,8 +255,8 @@ func SummaryProblem(summary string) string {
// withOptions is an ask with what every form of it carries: who answers, its expiry, what it is about, and its two
// options, each binding its act.
func (p Proposal) withOptions(a asks.Ask, does string) asks.Ask {
a.Who, a.Expires, a.OnExpiry = asks.Operator, p.At.Add(ProposalLasts), "the proposal is discarded; nothing changes"
func (p Proposal) withOptions(a oc.Ask, does string) oc.Ask {
a.Who, a.Expires, a.OnExpiry = occ.Operator, occ.FormatTime(p.At.Add(ProposalLasts)), "the proposal is discarded; nothing changes"
a.About = "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name
if len(p.Nodes) == 0 {
a.About = "instructions." + p.Scope + "." + p.Name
@@ -264,15 +265,15 @@ func (p Proposal) withOptions(a asks.Ask, does string) asks.Ask {
{OptionApprove, "Approve", does},
{OptionDecline, "Decline", "nothing changes; the proposal is discarded"},
} {
binds, _ := asks.ActDigest(p.Act(o.id))
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
binds, _ := occ.ActDigest(p.Act(o.id))
a.Options = append(a.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
}
return a
}
// listedAsk is the ask as it was composed before issue 639, the text in its explanation: composed only to hold the
// warrant of a proposal made then.
func (p Proposal) listedAsk() asks.Ask {
func (p Proposal) listedAsk() oc.Ask {
verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action]
shown, whole := excerpt(p.Text)
part := ""
@@ -280,7 +281,7 @@ func (p Proposal) listedAsk() asks.Ask {
part = " Part shown"
}
headline := fmt.Sprintf("%s Claude instructions '%s'?%s", verb, p.Name, part)
if len([]rune(headline)) > asks.HeadlineLength {
if len([]rune(headline)) > occ.HeadlineLength {
headline = verb + " Claude instructions?" + part
}
var b strings.Builder
@@ -311,8 +312,8 @@ func (p Proposal) listedAsk() asks.Ask {
if p.Action == ActionRemove {
does = "the instructions are removed from those sessions"
}
a := asks.Ask{ID: p.ID, Headline: headline, Explanation: strings.TrimRight(b.String(), "\n"), Who: asks.Operator,
Expires: p.At.Add(ProposalLasts), OnExpiry: "the proposal is discarded; nothing changes",
a := oc.Ask{ID: p.ID, Headline: headline, Explanation: strings.TrimRight(b.String(), "\n"), Who: occ.Operator,
Expires: occ.FormatTime(p.At.Add(ProposalLasts)), OnExpiry: "the proposal is discarded; nothing changes",
About: "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name}
if len(p.Nodes) == 0 {
a.About = "instructions." + p.Scope + "." + p.Name
@@ -321,8 +322,8 @@ func (p Proposal) listedAsk() asks.Ask {
{OptionApprove, "Approve", does},
{OptionDecline, "Decline", "nothing changes; the proposal is discarded"},
} {
binds, _ := asks.ActDigest(p.Act(o.id))
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
binds, _ := occ.ActDigest(p.Act(o.id))
a.Options = append(a.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
}
return a
}
@@ -433,7 +434,7 @@ func invisible(text string) string {
// write the record and ask for it under this module's name.
func ProposalProblem(p Proposal) string {
switch {
case !asks.UsableID(p.ID):
case !occ.UsableID(p.ID):
return "its id is not one"
case !itemName.MatchString(p.Name):
return "its name is not one"
@@ -838,21 +839,21 @@ func Propose(p Paths, in ProposeInput, store ProposalStore, bus AskBus, now time
if why := approverReady(bus); why != "" {
return refuse(why + "; " + AtTheTerminalInstead)
}
if waiting := waitingProposals(store, approved, now); waiting >= asks.MostOpen {
if waiting := waitingProposals(store, approved, now); waiting >= occ.MostOpen {
return refuse(fmt.Sprintf("%d proposals already wait for the operator's answer, and the operator is asked at most %d "+
"questions at once; claude_code_proposals lists them", waiting, asks.MostOpen))
"questions at once; claude_code_proposals lists them", waiting, occ.MostOpen))
}
if problem := ProposalProblem(prop); problem != "" {
return refuse(problem)
}
ask := prop.Ask()
if err := ask.Check(now); err != nil {
if err := occ.Check(ask, now); err != nil {
return refuse(err.Error())
}
if err := store.Create(prop.ID, prop); err != nil {
return nil, fmt.Errorf("the proposal could not be kept, so the operator was not asked: %w", err)
}
if err := bus.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
if err := bus.Publish(oc.AcceptSubjectAsk(Module), ask, ask.ID); err != nil {
prop.Asked = "not asked: " + err.Error()
_ = store.Put(prop.ID, prop)
say("proposal %s (%s %s %s) could not be asked: %v", prop.ID, prop.Action, prop.Scope, prop.Name, err)
@@ -874,11 +875,15 @@ func Propose(p Paths, in ProposeInput, store ProposalStore, bus AskBus, now time
}
time.Sleep(RouterAnswersEvery)
}
expires, err := askExpires(ask)
if err != nil {
return nil, err
}
return map[string]any{
"router": taken,
"proposed": prop.ID, "action": prop.Action, "reaches": prop.where(), "fingerprint": prop.Fingerprint(),
"asked": "the operator is asked on a channel that proves who answers: \"" + ask.Headline + "\"",
"expires": ask.Expires.Format(time.RFC3339),
"expires": expires.Format(time.RFC3339),
"until then": "nothing changes on any node; the text becomes part of those sessions only on the operator's Approve, " +
"and is discarded on Decline or at expiry. claude_code_proposals shows where it stands",
}, nil
@@ -927,10 +932,10 @@ var (
// read.
var (
endedMu sync.Mutex
ended = map[string]asks.Warrant{}
ended = map[string]oc.Warrant{}
)
func endedAsk(id string) (asks.Warrant, bool) {
func endedAsk(id string) (oc.Warrant, bool) {
endedMu.Lock()
defer endedMu.Unlock()
w, ok := ended[id]
@@ -940,8 +945,8 @@ func endedAsk(id string) (asks.Warrant, bool) {
// HeardEnd takes the router's word that one of this module's asks ended without a choice: refused, expired,
// cancelled or replaced. The proposal is discarded here at once. Nothing is applied on such a word, so it needs no
// proof beyond its subject; a choice is only ever taken from the router's record (Reconcile).
func HeardEnd(p Paths, store ProposalStore, w asks.Warrant, now time.Time) (string, error) {
if w.Asker != Module || w.Outcome == asks.OutcomeChosen || !asks.UsableID(w.Ask) {
func HeardEnd(p Paths, store ProposalStore, w oc.Warrant, now time.Time) (string, error) {
if w.Asker != Module || w.Outcome == occ.OutcomeChosen || !occ.UsableID(w.Ask) {
return "", nil
}
reconcileMu.Lock()
@@ -968,7 +973,7 @@ func HeardEnd(p Paths, store ProposalStore, w asks.Warrant, now time.Time) (stri
}
// Heard, not read from a record: Reconcile still reads the router's record of it, so an ask the router took after
// all (its queue offered it again once a channel was ready) is answered as any other.
d := ProposalDecision{At: now.UTC(), Says: w.Says(), Outcome: "discarded: the ask " + string(w.Outcome), Heard: true}
d := ProposalDecision{At: now.UTC(), Says: occ.Says(w), Outcome: "discarded: the ask " + string(w.Outcome), Heard: true}
if w.Words != "" {
d.Outcome += " (" + w.Words + ")"
}
@@ -986,9 +991,9 @@ func HeardEnd(p Paths, store ProposalStore, w asks.Warrant, now time.Time) (stri
// routerRecord is the router's record of one ask, as far as this module reads it.
type routerRecord struct {
State string `json:"state"`
Ask *asks.Ask `json:"ask"`
Warrant *asks.Warrant `json:"warrant"`
State string `json:"state"`
Ask *oc.Ask `json:"ask"`
Warrant *oc.Warrant `json:"warrant"`
}
var reconcileMu sync.Mutex
@@ -1010,7 +1015,7 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
}
type closed struct {
prop Proposal
w asks.Warrant
w oc.Warrant
}
var ready []closed
discarded := false
@@ -1027,8 +1032,8 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
if done {
// Discarded on the router's word alone: only a record of a person's choice reopens it.
var rec routerRecord
if err == nil && entry != nil && json.Unmarshal(entry.Value, &rec) == nil && rec.Warrant != nil &&
rec.Warrant.Outcome == asks.OutcomeChosen {
if err == nil && entry != nil && json.Unmarshal(entry.Value, &rec) == nil && rec.check() == nil && rec.Warrant != nil &&
rec.Warrant.Outcome == occ.OutcomeChosen {
ready = append(ready, closed{prop, *rec.Warrant})
}
continue
@@ -1046,12 +1051,16 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
continue // open, not yet recorded, or not readable now: asked again on the next look
}
var rec routerRecord
if json.Unmarshal(entry.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
if json.Unmarshal(entry.Value, &rec) != nil || rec.check() != nil || rec.State == "open" || rec.Warrant == nil {
continue
}
ready = append(ready, closed{prop, *rec.Warrant})
}
sort.SliceStable(ready, func(i, j int) bool { return ready[i].w.At.Before(ready[j].w.At) })
sort.SliceStable(ready, func(i, j int) bool {
a, _ := warrantAt(ready[i].w) // checked when read
b, _ := warrantAt(ready[j].w)
return a.Before(b)
})
changed, wrote := false, discarded
var said []string
for _, c := range ready {
@@ -1108,17 +1117,21 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
// decide holds one closed ask's warrant to its proposal, and applies it to this node's record when it holds. final
// is false when the answer did not hold here: it is judged again on the next look, and nothing is applied.
func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time.Time) (ProposalDecision, bool, string, bool) {
d := ProposalDecision{At: now.UTC(), Says: w.Says()}
func decide(p Paths, approved *Approved, prop Proposal, w oc.Warrant, now time.Time) (ProposalDecision, bool, string, bool) {
d := ProposalDecision{At: now.UTC(), Says: occ.Says(w)}
refused := func(why string) (ProposalDecision, bool, string, bool) {
d.Outcome = "refused: " + why
say("REFUSED the answer to proposal %s: %s", prop.ID, why)
return d, false, "proposal " + prop.ID + ": refused, " + why, false
}
at, err := warrantAt(w)
if err != nil {
return refused(err.Error())
}
if w.Asker != Module || w.Ask != prop.ID {
return refused(fmt.Sprintf("the router's record is for %s's ask %s", w.Asker, w.Ask))
}
if w.Outcome != asks.OutcomeChosen {
if w.Outcome != occ.OutcomeChosen {
d.Outcome = "discarded: the ask " + string(w.Outcome)
if w.Words != "" {
d.Outcome += " (" + w.Words + ")"
@@ -1130,11 +1143,11 @@ func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time
if problem := ProposalProblem(prop); problem != "" {
return refused("the proposal is not one this module makes: " + problem)
}
option, err := w.For(Module, prop.Ask())
option, err := occ.For(w, Module, prop.Ask())
if err != nil {
return refused(err.Error())
}
if err := option.Performs(prop.Act(option.ID)); err != nil {
if err := occ.Performs(option, prop.Act(option.ID)); err != nil {
return refused(err.Error())
}
d.Warranted = true
@@ -1146,7 +1159,7 @@ func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time
return refused("the option " + option.ID + " is not one a proposal offers")
}
key := sectionKey(prop.Scope, prop.Nodes, prop.Name)
if last, ok := approved.Changed[key]; ok && !w.At.After(last) {
if last, ok := approved.Changed[key]; ok && !at.After(last) {
d.Outcome = "approved, and superseded here: a change of this section approved later was applied first"
return d, false, "proposal " + prop.ID + ": " + d.Outcome, true
}
@@ -1157,16 +1170,16 @@ func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time
held, ok := approved.Sections[key]
if !ok || held.Digest != prop.Digest {
d.Outcome = "approved, nothing to remove here: this node holds no section " + prop.Name + " with that fingerprint"
approved.Changed[key] = w.At.UTC()
approved.Changed[key] = at.UTC()
return d, false, "proposal " + prop.ID + ": " + d.Outcome, true
}
delete(approved.Sections, key)
default:
approved.Sections[key] = ApprovedSection{Name: prop.Name, Scope: prop.Scope, Nodes: prop.Nodes, Text: prop.Text,
Digest: prop.Digest, Proposal: prop.ID, From: prop.From, Approved: w.At.UTC(), By: w.Says()}
Digest: prop.Digest, Proposal: prop.ID, From: prop.From, Approved: at.UTC(), By: occ.Says(w)}
}
approved.Changed[key] = w.At.UTC()
say("proposal %s (%s %s %s, fingerprint %s): %s", prop.ID, prop.Action, prop.Scope, prop.Name, prop.Fingerprint(), w.Says())
approved.Changed[key] = at.UTC()
say("proposal %s (%s %s %s, fingerprint %s): %s", prop.ID, prop.Action, prop.Scope, prop.Name, prop.Fingerprint(), occ.Says(w))
return d, reaches, "proposal " + prop.ID + ": approved, " + prop.Action + " " + prop.Name, true
}
@@ -1222,12 +1235,12 @@ func ListProposals(p Paths, store ProposalStore, bus AskBus, id string, now time
// the router took, both read now.
if entry, err := bus.Record(prop.ID); err == nil && entry != nil {
var rec routerRecord
if json.Unmarshal(entry.Value, &rec) == nil {
if json.Unmarshal(entry.Value, &rec) == nil && rec.check() == nil {
row["router"] = rec.State
switch {
case rec.Ask == nil:
row["the phone"] = "cannot be compared: the router's record keeps no ask"
case rec.Ask.Digest() == prop.Ask().Digest():
case occ.Digest(*rec.Ask) == occ.Digest(prop.Ask()):
row["the phone"] = "shows the ask composed from exactly this text (fingerprint " + prop.Fingerprint() + ")"
default:
row["the phone"] = "shows ANOTHER ask than this record composes: do not approve it"
@@ -13,8 +13,9 @@ import (
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// memProposals is the `proposals` state, shared by every node of a test as the bus shares it.
@@ -46,7 +47,7 @@ func (s *memProposals) Get(key string) (json.RawMessage, bool, error) {
// router is the operator channel's holder as the module reaches it: what was asked, its record of each ask, and
// the channel kinds.
type router struct {
published map[string]asks.Ask
published map[string]oc.Ask
subjects []string
records map[string]json.RawMessage
kinds []stdio.KindHeld
@@ -54,7 +55,7 @@ type router struct {
}
func newRouter() *router {
return &router{published: map[string]asks.Ask{}, records: map[string]json.RawMessage{},
return &router{published: map[string]oc.Ask{}, records: map[string]json.RawMessage{},
kinds: []stdio.KindHeld{{Seat: "channel", Kind: "telegram", Module: "telegram", Node: "novox",
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
}
@@ -65,7 +66,7 @@ func (r *router) Publish(subject string, body any, id string) error {
}
r.subjects = append(r.subjects, subject)
raw, _ := json.Marshal(body)
var a asks.Ask
var a oc.Ask
_ = json.Unmarshal(raw, &a)
r.published[a.ID] = a
r.records[a.ID], _ = json.Marshal(map[string]any{"state": "open"})
@@ -81,19 +82,19 @@ func (r *router) Record(id string) (*stdio.StateEntry, error) {
func (r *router) Kinds() ([]stdio.KindHeld, error) { return r.kinds, nil }
// close ends an ask with a warrant, as the router records it.
func (r *router) close(w asks.Warrant) {
func (r *router) close(w oc.Warrant) {
w.Asker = Module
state := string(w.Outcome)
r.records[w.Ask], _ = json.Marshal(map[string]any{"state": state, "warrant": w})
}
// chose is the warrant for a person's choice of an option of the ask as the router took it.
func (r *router) chose(id, option string, at time.Time) asks.Warrant {
func (r *router) chose(id, option string, at time.Time) oc.Warrant {
a := r.published[id]
o, _ := a.Option(option)
return asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label, Level: o.Level,
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"},
Channel: "telegram", Proofs: []string{"P1"}, At: at, AskDigest: a.Digest()}
o, _ := occ.OptionOf(a, option)
return oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeChosen, Option: o.ID, Label: o.Label, Level: o.Level,
By: &oc.Person{Who: occ.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"},
Channel: "telegram", Proofs: []string{"P1"}, At: occ.FormatTime(at), AskDigest: occ.Digest(a)}
}
type recorded struct{ asks []string }
@@ -144,13 +145,13 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
t.Fatalf("the ask was published on %v, not under this module's own name", r.subjects)
}
a := r.published[id]
if err := a.Check(t0); err != nil {
if err := occ.Check(a, t0); err != nil {
t.Fatalf("the router would refuse the ask: %v", err)
}
// At a glance (issue 639): what it does is the explanation, one line; the text is the Details, which the
// router notes collapsed after the answers.
if a.Explanation != "Every Claude session on the mesh reads these 4 lines as the mesh's own word, from its next start until they are removed the same way." ||
asks.Glance(a.Explanation) != nil {
occ.Glance(a.Explanation) != nil {
t.Errorf("the phone does not say what is asked at a glance: %q", a.Explanation)
}
if a.Headline != "Add Claude instructions 'handoff-from-g14'?" {
@@ -163,11 +164,11 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
t.Fatalf("options: %+v", a.Options)
}
for _, o := range a.Options {
if o.Level != asks.Approve || !strings.HasPrefix(o.Binds, "sha256:") {
if o.Level != occ.Approve || !strings.HasPrefix(o.Binds, "sha256:") {
t.Errorf("the option %s is %s binding %q: both answers need a proven sender and bind the act", o.ID, o.Level, o.Binds)
}
}
if !a.Expires.Equal(t0.Add(ProposalLasts)) {
if a.Expires != occ.FormatTime(t0.Add(ProposalLasts)) {
t.Errorf("expires %s", a.Expires)
}
// Nothing is active, here or anywhere, while the ask is open.
@@ -212,10 +213,10 @@ func TestNothingBecomesActiveWithoutAValidWarrant(t *testing.T) {
}{
{"declined", func(r *router, _ *memProposals, id string) { r.close(r.chose(id, OptionDecline, t0.Add(time.Hour))) }, "declined"},
{"expired", func(r *router, _ *memProposals, id string) {
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeExpired, At: t0.Add(24 * time.Hour)})
r.close(oc.Warrant{Ask: id, Outcome: occ.OutcomeExpired, At: occ.FormatTime(t0.Add(24 * time.Hour))})
}, "discarded"},
{"refused by the router", func(r *router, _ *memProposals, id string) {
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeRefused, Words: "no channel can carry it", At: t0})
r.close(oc.Warrant{Ask: id, Outcome: occ.OutcomeRefused, Words: "no channel can carry it", At: occ.FormatTime(t0)})
}, "discarded"},
{"the text changed after the ask, with its digest", func(r *router, store *memProposals, id string) {
var prop Proposal
@@ -243,7 +244,7 @@ func TestNothingBecomesActiveWithoutAValidWarrant(t *testing.T) {
}, "refused"},
{"answered at the level acknowledge", func(r *router, _ *memProposals, id string) {
w := r.chose(id, OptionApprove, t0.Add(time.Hour))
w.Level = asks.Acknowledge
w.Level = occ.Acknowledge
r.close(w)
}, "refused"},
{"no person chose", func(r *router, _ *memProposals, id string) {
@@ -428,7 +429,7 @@ func TestALongTextIsShownInPartAndReadWholeAtTheTerminal(t *testing.T) {
!strings.Contains(a.Details, "fingerprint "+out["fingerprint"].(string)) {
t.Errorf("the phone does not say where to read all of it: %q", a.Details)
}
if len(a.Details) > 3000 || asks.Glance(a.Explanation) != nil {
if len(a.Details) > 3000 || occ.Glance(a.Explanation) != nil {
t.Errorf("the details are %d bytes; the explanation %q", len(a.Details), a.Explanation)
}
list, err := ListProposals(p, store, r, id, t0)
@@ -496,7 +497,7 @@ func TestATextChangedPastWhatThePhoneShowsIsRefused(t *testing.T) {
_ = json.Unmarshal(store.m[id], &prop)
prop.Text = strings.Replace(prop.Text, "Ask before you push.", "Push without asking.", 1)
_ = store.Put(id, prop)
if prop.Ask().Digest() != r.published[id].Digest() {
if occ.Digest(prop.Ask()) != occ.Digest(r.published[id]) {
t.Fatal("the test means a change the ask's words do not show")
}
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
@@ -559,7 +560,7 @@ func TestAForgedRecordIsNotAppliedEvenWhenApproved(t *testing.T) {
} {
prop.Digest = textDigest(prop.Text)
_ = store.Put(prop.ID, prop)
if err := r.Publish(asks.AskSubject(Module), prop.Ask(), prop.ID); err != nil {
if err := r.Publish(oc.AcceptSubjectAsk(Module), prop.Ask(), prop.ID); err != nil {
t.Fatal(err)
}
r.close(r.chose(prop.ID, OptionApprove, t0.Add(time.Hour)))
@@ -718,8 +719,8 @@ func (r *refusingRouter) Publish(subject string, body any, id string) error {
r.id = id
go func() {
time.Sleep(50 * time.Millisecond)
_, _ = HeardEnd(r.p, r.store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused,
Words: "no channel can carry any of its answers now: telegram is not linked", At: t0}, t0)
_, _ = HeardEnd(r.p, r.store, oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeRefused,
Words: "no channel can carry any of its answers now: telegram is not linked", At: occ.FormatTime(t0)}, t0)
}()
return nil
}
@@ -729,12 +730,23 @@ func TestAnAskTakenAfterItsRefusalIsStillAnswered(t *testing.T) {
p, _ := node(t, "novox")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
if _, err := HeardEnd(p, store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused, Words: "no channel", At: t0}, t0); err != nil {
if _, err := HeardEnd(p, store, oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeRefused, Words: "no channel", At: occ.FormatTime(t0)}, t0); err != nil {
t.Fatal(err)
}
if got := outcomeHere(t, p, id); !strings.HasPrefix(got, "discarded") {
t.Fatalf("decided %q", got)
}
// A warrant whose time does not parse is refused, naming the field, as the decode of it was.
bad := r.chose(id, OptionApprove, t0.Add(time.Hour))
bad.At = "yesterday"
prop, _ := readProposal(store, id)
appr := Approved{Decided: map[string]ProposalDecision{}, Changed: map[string]time.Time{}, Refused: map[string]string{}}
if d, _, _, _ := decide(p, &appr, prop, bad, t0.Add(time.Hour)); !strings.HasPrefix(d.Outcome, "refused: the warrant's at") {
t.Errorf("a warrant with a malformed at was decided %q", d.Outcome)
}
if (routerRecord{Warrant: &bad}).check() == nil {
t.Error("a router record with a malformed at passed its check")
}
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(time.Hour)); !changed {
t.Fatal("the approval of an ask taken after its refusal was lost")
@@ -769,7 +781,7 @@ func TestAProposalIsAskedAtAGlanceAndAnOlderOneAsItWasAsked(t *testing.T) {
older := prop
older.Form = 0
listed := older.Ask()
if !strings.Contains(listed.Explanation, "The whole text:") || listed.Details != "" || listed.Digest() == prop.Ask().Digest() {
if !strings.Contains(listed.Explanation, "The whole text:") || listed.Details != "" || occ.Digest(listed) == occ.Digest(prop.Ask()) {
t.Errorf("a proposal made before is not composed as it was asked: %q", listed.Explanation)
}
}
@@ -11,7 +11,7 @@ package main
//
// The session reads the answer by the question's key with claude_code_answer: from the router's own record of the
// ask, which only the router writes and only this module reads, held to the question as the `questions` state keeps
// it (asks.Warrant.For and asks.Option.Performs). A record that does not hold is said as not proven, never as a
// it (oc.Warrant.For and oc.Option.Performs). A record that does not hold is said as not proven, never as a
// choice. An answer informs the session; it authorises nothing (ADR 0286 stays the way to an approved call).
import (
@@ -27,8 +27,9 @@ import (
"sync"
"time"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// QuestionsState is the module's state the questions are kept in.
@@ -38,16 +39,16 @@ const QuestionsState = "questions"
const QuestionPrefix = "q-"
// MostQuestions is how many questions may wait at once, mesh-wide: the router allows an asker three open asks
// (asks.MostOpen), and questions alone never fill them, so one is left for a proposed instruction section (ADR 0274)
// (occ.MostOpen), and questions alone never fill them, so one is left for a proposed instruction section (ADR 0274)
// while two questions are open. Each open question holds one slot key in the `questions` state (slotKey), taken by
// create or by compare-and-set, so two nodes asking at once cannot both take the last one.
const MostQuestions = asks.MostOpen - 1
const MostQuestions = occ.MostOpen - 1
// A question the router holds no record of, and whose refusal was not heard here, keeps its slot until it expires:
// the router may still have it in its queue (down, or backlogged), and neither the queue's order nor a cancel can
// take it back — the queue offers a piece of work again after an error, so a cancel may be taken before the ask it
// cancels, and a cancel of an ask not yet recorded is dropped. Once the question has expired the router refuses it
// when it is taken (asks.Ask.Check), so the slot is free and the question has ended.
// when it is taken (oc.Ask.Check), so the slot is free and the question has ended.
// slotKey is the key of one of the MostQuestions slots: not a question's key, so no question is ever read from it.
func slotKey(n int) string { return fmt.Sprintf("slot-%d", n) }
@@ -69,7 +70,7 @@ const (
// The bounds of what a question says.
const (
FewestQuestionOptions = 2
MostContextBytes = asks.SummaryLength
MostContextBytes = occ.SummaryLength
MostMeansBytes = 200
MostWhyBytes = 120
MostOnExpiryBytes = 200
@@ -151,8 +152,8 @@ func (q Question) recommended() (QuestionOption, bool) {
}
// Act is what an answer binds: this question, whole, and the option chosen.
func (q Question) Act(option string) asks.Act {
act := asks.Act{"verb": Module + ".question", "option": option, "question": q.ID, "headline": q.Headline,
func (q Question) Act(option string) occ.Act {
act := occ.Act{"verb": Module + ".question", "option": option, "question": q.ID, "headline": q.Headline,
"context": q.Context, "why": q.Why, "on-expiry": q.OnExpiry, "by": q.By, "from": q.From,
"at": q.At.UTC().Format(time.RFC3339Nano), "expires": q.Expires.UTC().Format(time.RFC3339Nano),
"options": strconv.Itoa(len(q.Options))}
@@ -163,7 +164,7 @@ func (q Question) Act(option string) asks.Act {
}
// Ask is the ask for a question, composed from it alone, in the form it was asked in.
func (q Question) Ask() asks.Ask {
func (q Question) Ask() oc.Ask {
if q.Form == AskGlance {
return q.glanceAsk()
}
@@ -172,31 +173,31 @@ func (q Question) Ask() asks.Ask {
// glanceAsk is the question read at a glance on the phone (novox/hq issue 639): its context, then the recommended
// answer and why as its one point; who asked and what the answer does are its Details.
func (q Question) glanceAsk() asks.Ask {
func (q Question) glanceAsk() oc.Ask {
explanation := strings.TrimSpace(q.Context)
if rec, ok := q.recommended(); ok {
explanation += fmt.Sprintf("\n• Recommended: %s, because %s.", rec.Label, strings.TrimRight(strings.TrimSpace(q.Why), "."))
}
details := fmt.Sprintf("Asked by the agent session %s on %s through the claude-code module, at %s UTC. Your answer is told "+
"to that session; it performs nothing by itself.", q.By, q.From, q.At.UTC().Format("15:04 on 2 Jan"))
return q.withOptions(asks.Ask{ID: q.ID, Headline: q.Headline, Explanation: explanation, Details: details})
return q.withOptions(oc.Ask{ID: q.ID, Headline: q.Headline, Explanation: explanation, Details: details})
}
func (q Question) withOptions(a asks.Ask) asks.Ask {
a.Who, a.Expires, a.OnExpiry = asks.Operator, q.Expires, q.OnExpiry
func (q Question) withOptions(a oc.Ask) oc.Ask {
a.Who, a.Expires, a.OnExpiry = occ.Operator, occ.FormatTime(q.Expires), q.OnExpiry
for _, o := range q.Options {
does := o.Means
if o.Recommended {
does = "Recommended. " + does
}
binds, _ := asks.ActDigest(q.Act(o.ID))
a.Options = append(a.Options, asks.Option{ID: o.ID, Label: o.Label, Does: does, Level: asks.Approve, Binds: binds})
binds, _ := occ.ActDigest(q.Act(o.ID))
a.Options = append(a.Options, oc.Option{ID: o.ID, Label: o.Label, Does: does, Level: occ.Approve, Binds: binds})
}
return a
}
// listedAsk is the ask as it was composed before issue 639: composed only to hold the answer to a question asked then.
func (q Question) listedAsk() asks.Ask {
func (q Question) listedAsk() oc.Ask {
var b strings.Builder
b.WriteString(strings.TrimSpace(q.Context))
if rec, ok := q.recommended(); ok {
@@ -205,15 +206,15 @@ func (q Question) listedAsk() asks.Ask {
}
fmt.Fprintf(&b, "\n\nAsked by the agent session %s on %s through the claude-code module, at %s UTC. "+
"Your answer is told to that session; it performs nothing by itself.", q.By, q.From, q.At.UTC().Format("15:04 on 2 Jan"))
a := asks.Ask{ID: q.ID, Headline: q.Headline, Explanation: b.String(), Who: asks.Operator, Expires: q.Expires,
a := oc.Ask{ID: q.ID, Headline: q.Headline, Explanation: b.String(), Who: occ.Operator, Expires: occ.FormatTime(q.Expires),
OnExpiry: q.OnExpiry}
for _, o := range q.Options {
does := o.Means
if o.Recommended {
does = "Recommended. " + does
}
binds, _ := asks.ActDigest(q.Act(o.ID))
a.Options = append(a.Options, asks.Option{ID: o.ID, Label: o.Label, Does: does, Level: asks.Approve, Binds: binds})
binds, _ := occ.ActDigest(q.Act(o.ID))
a.Options = append(a.Options, oc.Option{ID: o.ID, Label: o.Label, Does: does, Level: occ.Approve, Binds: binds})
}
return a
}
@@ -228,8 +229,8 @@ func QuestionProblem(q Question) string {
switch {
case strings.TrimSpace(q.Headline) == "":
add("it has no headline")
case len([]rune(q.Headline)) > asks.HeadlineLength:
add("its headline is %d characters, more than %d", len([]rune(q.Headline)), asks.HeadlineLength)
case len([]rune(q.Headline)) > occ.HeadlineLength:
add("its headline is %d characters, more than %d", len([]rune(q.Headline)), occ.HeadlineLength)
}
switch {
case strings.TrimSpace(q.Context) == "":
@@ -239,16 +240,16 @@ func QuestionProblem(q Question) string {
case strings.Contains(strings.TrimSpace(q.Context), "\n"):
add("its context is more than one line: a phone shows it at a glance as one short paragraph")
}
if n := len(q.Options); n < FewestQuestionOptions || n > asks.MostOptions {
add("it offers %d options; a question offers %d to %d", n, FewestQuestionOptions, asks.MostOptions)
if n := len(q.Options); n < FewestQuestionOptions || n > occ.MostOptions {
add("it offers %d options; a question offers %d to %d", n, FewestQuestionOptions, occ.MostOptions)
}
recommended, labels := 0, map[string]bool{}
for i, o := range q.Options {
switch {
case strings.TrimSpace(o.Label) == "":
add("option %d has no label", i+1)
case len([]rune(o.Label)) > asks.LabelLength:
add("option %d's label is longer than %d characters", i+1, asks.LabelLength)
case len([]rune(o.Label)) > occ.LabelLength:
add("option %d's label is longer than %d characters", i+1, occ.LabelLength)
case labels[strings.ToLower(o.Label)]:
add("two options are labelled %q", o.Label)
}
@@ -258,7 +259,7 @@ func QuestionProblem(q Question) string {
} else if len(o.Means) > MostMeansBytes {
add("what option %d means is longer than %d bytes", i+1, MostMeansBytes)
}
if !asks.UsableID(o.ID) {
if !occ.UsableID(o.ID) {
add("option %d's id is not one", i+1)
}
if o.Recommended {
@@ -368,7 +369,7 @@ func AskQuestion(p Paths, in QuestionInput, store QuestionStore, bus AskBus, now
return refuse(problem)
}
ask := q.Ask()
if err := ask.Check(now); err != nil {
if err := occ.Check(ask, now); err != nil {
return refuse(err.Error())
}
// The question is kept before its slot is taken, so a slot always names a question another node can judge.
@@ -384,7 +385,7 @@ func AskQuestion(p Paths, in QuestionInput, store QuestionStore, bus AskBus, now
"at once, so one of the router's three open asks is left for a proposed instruction section; read them with "+
"claude_code_answer, or wait for one to end", len(open), strings.Join(open, ", "), MostQuestions))
}
if err := bus.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
if err := bus.Publish(oc.AcceptSubjectAsk(Module), ask, ask.ID); err != nil {
_ = store.Put(q.ID, tombstone(q, "not asked: "+err.Error()))
return nil, fmt.Errorf("the operator could not be asked: %v", err)
}
@@ -517,10 +518,10 @@ func readQuestionErr(store QuestionStore, key string) (Question, bool, error) {
// The router's word that a question ended without a choice, as heard on the decided subject.
var (
questionEndsMu sync.Mutex
questionEnds = map[string]asks.Warrant{}
questionEnds = map[string]oc.Warrant{}
)
func questionEnd(id string) (asks.Warrant, bool) {
func questionEnd(id string) (oc.Warrant, bool) {
questionEndsMu.Lock()
defer questionEndsMu.Unlock()
w, ok := questionEnds[id]
@@ -537,8 +538,8 @@ func forgetQuestionEnd(id string) {
// replaced — and keeps it on the question, because a refused ask leaves no record of the router's. A refused question
// keeps none of its words (tombstone). A choice is never taken from this word: only from the router's record, held to
// the question (ReadAnswer).
func HeardQuestionEnd(store QuestionStore, w asks.Warrant) {
if w.Asker != Module || w.Outcome == asks.OutcomeChosen || !strings.HasPrefix(w.Ask, QuestionPrefix) {
func HeardQuestionEnd(store QuestionStore, w oc.Warrant) {
if w.Asker != Module || w.Outcome == occ.OutcomeChosen || !strings.HasPrefix(w.Ask, QuestionPrefix) {
return
}
q, known := readQuestion(store, w.Ask)
@@ -550,7 +551,7 @@ func HeardQuestionEnd(store QuestionStore, w asks.Warrant) {
if w.Words != "" {
ended += ": " + w.Words
}
if w.Outcome == asks.OutcomeRefused {
if w.Outcome == occ.OutcomeRefused {
q = tombstone(q, ended)
} else {
q.Ended = ended
@@ -589,7 +590,10 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
}
var rec routerRecord
if entry != nil {
if err := json.Unmarshal(entry.Value, &rec); err != nil {
if err := json.Unmarshal(entry.Value, &rec); err != nil || rec.check() != nil {
if err == nil {
err = rec.check()
}
out["state"], out["words"] = AnswerNotProven, "the router's record of it cannot be read: "+err.Error()
return out, nil
}
@@ -613,12 +617,12 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
}
default:
w := *rec.Warrant
if w.Outcome != asks.OutcomeChosen {
if w.Outcome != occ.OutcomeChosen {
out["state"], out["words"] = AnswerEnded, string(w.Outcome)
if w.Words != "" {
out["words"] = string(w.Outcome) + ": " + w.Words
}
if w.Outcome == asks.OutcomeExpired {
if w.Outcome == occ.OutcomeExpired {
out["now"] = "do what you said: " + q.OnExpiry
}
return out, nil
@@ -627,9 +631,9 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
out["state"], out["words"] = AnswerNotProven, "the question kept is not one this module asks: "+problem
return out, nil
}
option, err := w.For(Module, q.Ask())
option, err := occ.For(w, Module, q.Ask())
if err == nil {
err = option.Performs(q.Act(option.ID))
err = occ.Performs(option, q.Act(option.ID))
}
if err != nil {
say("REFUSED the answer to question %s: %v", q.ID, err)
@@ -642,8 +646,9 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
chosen["means"], chosen["recommended"] = o.Means, o.Recommended
}
}
out["state"], out["chosen"], out["by"], out["channel"], out["at"] = AnswerChosen, chosen, w.Says(), w.Channel,
w.At.Format(time.RFC3339)
at, _ := warrantAt(w) // checked with the record above
out["state"], out["chosen"], out["by"], out["channel"], out["at"] = AnswerChosen, chosen, occ.Says(w), w.Channel,
at.Format(time.RFC3339)
out["note"] = "the operator's word to this session; it authorises no call that needs the operator's approval"
}
return out, nil
@@ -663,3 +668,37 @@ func WaitForAnswer(store QuestionStore, bus AskBus, key string, wait time.Durati
time.Sleep(AnswerLooksEvery)
}
}
// warrantAt is when a warrant was made; the contract carries it as an RFC 3339 string, and one that does not parse is
// refused, as the decode of the router's word was before the contract carried it as a string.
func warrantAt(w oc.Warrant) (time.Time, error) {
t, err := occ.Time(w.At)
if err != nil {
return time.Time{}, fmt.Errorf("the warrant's at %q is not a time: %w", w.At, err)
}
return t, nil
}
// askExpires is when an ask ends, read from the contract's RFC 3339 string; one that does not parse is refused.
func askExpires(a oc.Ask) (time.Time, error) {
t, err := occ.Time(a.Expires)
if err != nil {
return time.Time{}, fmt.Errorf("the ask's expires %q is not a time: %w", a.Expires, err)
}
return t, nil
}
// check refuses a router record whose warrant or ask carries a time that does not parse.
func (r routerRecord) check() error {
if r.Warrant != nil {
if _, err := warrantAt(*r.Warrant); err != nil {
return err
}
}
if r.Ask != nil {
if _, err := askExpires(*r.Ask); err != nil {
return err
}
}
return nil
}
@@ -12,8 +12,9 @@ import (
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
func mergeTonight() QuestionInput {
@@ -50,18 +51,18 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
if !strings.HasPrefix(key, QuestionPrefix) || !strings.Contains(out["router"].(string), "took the question") {
t.Fatalf("answered %v", out)
}
if len(r.subjects) != 1 || r.subjects[0] != asks.AskSubject(Module) {
if len(r.subjects) != 1 || r.subjects[0] != oc.AcceptSubjectAsk(Module) {
t.Fatalf("published on %v", r.subjects)
}
a := r.published[key]
if a.Who != asks.Operator || a.OnExpiry != "I leave the pull request open" || !a.Expires.Equal(t0.Add(QuestionDefault)) {
if a.Who != occ.Operator || a.OnExpiry != "I leave the pull request open" || a.Expires != occ.FormatTime(t0.Add(QuestionDefault)) {
t.Errorf("ask %+v", a)
}
if len(a.Options) != 2 {
t.Fatalf("options %+v", a.Options)
}
for _, o := range a.Options {
if o.Level != asks.Approve || o.Binds == "" {
if o.Level != occ.Approve || o.Binds == "" {
t.Errorf("option %s is %s binding %q: every option is an approval bound to the question", o.ID, o.Level, o.Binds)
}
}
@@ -69,7 +70,7 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
t.Errorf("the recommendation is not on the recommended option alone: %+v", a.Options)
}
// At a glance (issue 639): the context and the recommendation are the explanation; who asks is the Details.
if !strings.HasSuffix(a.Explanation, "\n• Recommended: Merge now, because it is reviewed and the checks are green.") || asks.Glance(a.Explanation) != nil {
if !strings.HasSuffix(a.Explanation, "\n• Recommended: Merge now, because it is reviewed and the checks are green.") || occ.Glance(a.Explanation) != nil {
t.Errorf("the explanation does not read at a glance:\n%s", a.Explanation)
}
for _, want := range []string{"the agent session fix-589 on g14", "performs nothing by itself"} {
@@ -77,7 +78,7 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
t.Errorf("the details do not say %q:\n%s", want, a.Details)
}
}
if err := a.Check(t0); err != nil {
if err := occ.Check(a, t0); err != nil {
t.Errorf("the ask does not pass the asks contract: %v", err)
}
}
@@ -212,8 +213,8 @@ func (r *questionRefusingRouter) Publish(subject string, body any, id string) er
r.id = id
go func() {
time.Sleep(50 * time.Millisecond)
HeardQuestionEnd(r.store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused,
Words: "its words carry path (a file path), which may not leave the mesh", At: t0})
HeardQuestionEnd(r.store, oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeRefused,
Words: "its words carry path (a file path), which may not leave the mesh", At: occ.FormatTime(t0)})
}()
return nil
}
@@ -247,7 +248,7 @@ func TestAnExpiredQuestionSaysWhatTheSessionDoes(t *testing.T) {
p, _ := node(t, "g14")
store, r := newMemQuestions(), newRouter()
key := askMergeTonight(t, p, store, r, t0)
r.close(asks.Warrant{Ask: key, Outcome: asks.OutcomeExpired, At: t0.Add(QuestionDefault)})
r.close(oc.Warrant{Ask: key, Outcome: occ.OutcomeExpired, At: occ.FormatTime(t0.Add(QuestionDefault))})
out, err := ReadAnswer(store, r, key, t0.Add(QuestionDefault+time.Minute))
if err != nil || out["state"] != AnswerEnded || out["now"] != "do what you said: I leave the pull request open" {
t.Errorf("expired: %v %v", out, err)
@@ -276,7 +277,7 @@ func TestTheQuestionToolsAreServed(t *testing.T) {
var asked map[string]any
_ = json.Unmarshal([]byte(text), &asked)
key, _ := asked["key"].(string)
if a := r.published[key]; !a.Expires.Equal(t0.Add(30 * time.Minute)) {
if a := r.published[key]; a.Expires != occ.FormatTime(t0.Add(30*time.Minute)) {
t.Errorf("expires %v", a.Expires)
}
r.close(r.chose(key, "o1", t0.Add(time.Minute)))
@@ -381,10 +382,10 @@ func TestTheEdgesOfAQuestionAreAsked(t *testing.T) {
// A warrant naming another asker or another ask, or an option the question did not offer, is no answer to it.
func TestAWarrantForAnotherAskOrOptionIsNotProven(t *testing.T) {
p, _ := node(t, "g14")
for name, change := range map[string]func(*asks.Warrant){
"another asker": func(w *asks.Warrant) { w.Asker = "mesh-controller" },
"another ask": func(w *asks.Warrant) { w.Ask = "q-0000000000000000" },
"an option unoffered": func(w *asks.Warrant) { w.Option, w.Label = "o9", "Delete everything" },
for name, change := range map[string]func(*oc.Warrant){
"another asker": func(w *oc.Warrant) { w.Asker = "mesh-controller" },
"another ask": func(w *oc.Warrant) { w.Ask = "q-0000000000000000" },
"an option unoffered": func(w *oc.Warrant) { w.Option, w.Label = "o9", "Delete everything" },
} {
store, r := newMemQuestions(), newRouter()
key := askMergeTonight(t, p, store, r, t0)
@@ -474,7 +475,7 @@ func TestASlotIsTakenOnlyAtTheRevisionRead(t *testing.T) {
store, r := newMemQuestions(), newRouter()
// Two questions that ended: their slots are free to take again.
for _, id := range []string{askMergeTonight(t, p, store, r, t0), askMergeTonight(t, p, store, r, t0)} {
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeExpired, At: t0.Add(time.Minute)})
r.close(oc.Warrant{Ask: id, Outcome: occ.OutcomeExpired, At: occ.FormatTime(t0.Add(time.Minute))})
}
shanks, _ := node(t, "shanks")
other := newRouter()
@@ -487,7 +488,7 @@ func TestASlotIsTakenOnlyAtTheRevisionRead(t *testing.T) {
From: shanks.Node, At: t0, Expires: t0.Add(time.Hour),
Options: []QuestionOption{{ID: "o1", Label: "A", Means: "a", Recommended: true}, {ID: "o2", Label: "B", Means: "b"}}}
_ = store.Put(q.ID, q)
_ = other.Publish(asks.AskSubject(Module), q.Ask(), q.ID)
_ = other.Publish(oc.AcceptSubjectAsk(Module), q.Ask(), q.ID)
r.records[q.ID] = other.records[q.ID]
_ = store.Put(key, slot{Question: q.ID, At: t0})
}
+9 -4
View File
@@ -2,12 +2,17 @@ module claude-code
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require gopkg.in/yaml.v3 v3.0.1
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104174344-ae5266e77639
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/klauspost/compress v1.20.1 // 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
+22 -4
View File
@@ -1,7 +1,23 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-20261104174344-ae5266e77639 h1:19PhtrCOwia711Oqdyafvs5EKTVmmgk5Gdb4leCkq3w=
git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-20261104174344-ae5266e77639/go.mod h1:Dj6fbL8Evgy72EWO3Gl3L3Ra8N/nJSzgEVwSGmeRam0=
git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104174344-ae5266e77639 h1:K/1yIkqAWKGtLEtJ0PibijIdpo5YATL/9IUVXFoEglM=
git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104174344-ae5266e77639/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
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=
@@ -12,6 +28,8 @@ 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=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+19 -7
View File
@@ -23,7 +23,16 @@
"operator-channel"
],
"invokes": [
"seat:mesh-hand-acts.warranted"
"seat:mesh-hand-acts.warranted",
"seat:mesh-hand-acts.hand-act",
"mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-assigner.assigner_read_placement"
],
"consumes": [
"mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.manifests",
"mesh-delivery-assigner.views.placement"
],
"state": [
"servers",
@@ -32,13 +41,14 @@
"proposals",
"questions",
"memory",
"waits"
"waits",
"findings"
],
"emits": [
"judged"
],
"reads": [
"claude-licence-manager.bindings",
"mesh-delivery-assigner.placement",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests"
"claude-licence-manager.bindings"
],
"tools": [
"claude_code_status",
@@ -89,7 +99,9 @@
"claude_code_home_removed",
"claude_code_home_restore",
"claude_code_judge",
"claude_code_judged"
"claude_code_judged",
"claude_code_first_views",
"claude_code_first_view_again"
],
"data": {
"own": [
+2 -2
View File
@@ -3,7 +3,7 @@ module claude-licence-manager
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/jackc/pgx/v5 v5.11.0
)
@@ -11,7 +11,7 @@ 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
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,5 +1,7 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
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,8 +13,8 @@ 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/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module clipmenu
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module cups
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -3,12 +3,12 @@ module dbus
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/godbus/dbus/v5 v5.1.0
)
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,9 +1,11 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
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/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+21 -21
View File
@@ -35,7 +35,7 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/channel"
)
// Kind is this channel's kind on both benches.
@@ -59,19 +59,19 @@ const StandingEvery = 5 * time.Minute
// Standing is what the desk says of itself: the bench's word, and when a named machine's notifier last lost
// the answers its notifications waited for (its bundle started again). The router then says again what is
// still open and unanswered (issue 384). It belongs in asks.Standing once the SDK carries it; until then the
// still open and unanswered (issue 384). It belongs in channel.Standing once the SDK carries it; until then the
// field travels as JSON the router reads under the same name. Zero (as "0001-01-01T00:00:00Z") until then.
type Standing struct {
asks.Standing
channel.Standing
AnswersLostAt time.Time `json:"answers-lost-at"`
}
// Shown is one of the router's messages as the desk shows it: the message, and its notification on each
// machine.
type Shown struct {
Message asks.Message `json:"message"`
On map[string]int `json:"on"`
Sent time.Time `json:"sent"`
Message channel.Message `json:"message"`
On map[string]int `json:"on"`
Sent time.Time `json:"sent"`
}
// Store keeps what is shown, by the router's handle.
@@ -146,7 +146,7 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
d.Logf("[desk-channel] work on %s is not this kind's; taken and dropped", subject)
return nil
}
var m asks.Message
var m channel.Message
if err := json.Unmarshal(body, &m); err != nil || m.Handle == "" {
d.Logf("[desk-channel] work on %s could not be read; taken and dropped", subject)
return nil
@@ -158,8 +158,8 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
// would appear later, to whoever is at the screen then.
seeing, why := d.seeing()
if len(seeing) == 0 {
f := asks.Failed{Handle: m.Handle, Why: "not shown: " + why, At: d.Now()}
return d.Publish(asks.IntakeSubject(asks.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10))
f := channel.Failed{Handle: m.Handle, Why: "not shown: " + why, At: d.Now()}
return d.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10))
}
machines = seeing
}
@@ -176,7 +176,7 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
// shown (a reminder, an escalation), replace the notification it had on each machine — dunst's replace id —
// so a problem is one notification updated in place, never one more beside it. An edit stays quiet where
// it was shown; a message said again is shown anew where it was not.
edit := parts[4] == asks.Edit
edit := parts[4] == channel.Edit
var before *Shown
switch {
case edit:
@@ -226,8 +226,8 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
if len(machines) == 0 {
why = "no machine to show it on: the setting desktop-machines names none"
}
f := asks.Failed{Handle: m.Handle, Why: why, At: d.Now()}
if err := d.Publish(asks.IntakeSubject(asks.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10)); err != nil {
f := channel.Failed{Handle: m.Handle, Why: why, At: d.Now()}
if err := d.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10)); err != nil {
return err
}
return nil
@@ -322,7 +322,7 @@ func sameShown(a, b Shown) bool {
}
// show puts one message on one machine's notifier, in place of `replace` when it names one.
func (d *Desk) show(machine string, m asks.Message, replace int) (int, error) {
func (d *Desk) show(machine string, m channel.Message, replace int) (int, error) {
urgency := "normal"
switch {
case m.Urgent:
@@ -376,12 +376,12 @@ func (d *Desk) Chosen(event, machine, ticket string) {
}
handle, shown := d.find(ticket)
if shown == nil {
_, _ = d.show(machine, asks.Message{Title: "That answer was not taken",
_, _ = d.show(machine, channel.Message{Title: "That answer was not taken",
Body: "It belongs to no message shown here any more. Nothing was done.", Silent: true}, 0)
return
}
if _, here := shown.On[machine]; !here {
_, _ = d.show(machine, asks.Message{Title: "That answer was not taken",
_, _ = d.show(machine, channel.Message{Title: "That answer was not taken",
Body: "It was not shown on " + machine + ". Nothing was done.", Silent: true}, 0)
return
}
@@ -395,11 +395,11 @@ func (d *Desk) Chosen(event, machine, ticket string) {
}
d.clickedOn[id] = machine
d.mu.Unlock()
c := asks.Choice{ID: id, Ticket: ticket, Handle: handle, At: d.Now(),
Sender: asks.Sender{Identity: machine, Display: machine, Authenticated: false}}
if err := d.Publish(asks.IntakeSubject(asks.Chosen, Kind), c, id); err != nil {
c := channel.Choice{ID: id, Ticket: ticket, Handle: handle, At: d.Now(),
Sender: channel.Sender{Identity: machine, Display: machine, Authenticated: false}}
if err := d.Publish(channel.IntakeSubject(channel.Chosen, Kind), c, id); err != nil {
d.Logf("[desk-channel] a choice could not be said to the router: %v", err)
_, _ = d.show(machine, asks.Message{Title: "That answer could not be taken now",
_, _ = d.show(machine, channel.Message{Title: "That answer could not be taken now",
Body: "The mesh did not take it. Try again in a minute.", Silent: true}, 0)
}
}
@@ -508,7 +508,7 @@ func (d *Desk) seeing() ([]string, string) {
// (the review of 2026-10-09). The router shows no link's code while the desk is not ready.
func (d *Desk) Standing() Standing {
d.init()
s := Standing{Standing: asks.Standing{At: d.Now()}, AnswersLostAt: d.answersLostAt()}
s := Standing{Standing: channel.Standing{At: d.Now()}, AnswersLostAt: d.answersLostAt()}
if _, why := d.seeing(); why != "" {
s.Why = why
return s
@@ -542,7 +542,7 @@ func (d *Desk) SayStanding() {
if same {
return
}
if err := d.Publish(asks.IntakeSubject(asks.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
if err := d.Publish(channel.IntakeSubject(channel.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
d.Logf("[desk-channel] could not say whether it can send: %v", err)
return
}
@@ -10,7 +10,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/channel"
)
type memShown map[string]Shown
@@ -62,10 +62,10 @@ func newDesk(t *testing.T, machines ...string) (*Desk, *[]notified, map[string][
return d, &shown, said, store
}
func take(t *testing.T, d *Desk, verb string, m asks.Message) {
func take(t *testing.T, d *Desk, verb string, m channel.Message) {
t.Helper()
raw, _ := json.Marshal(m)
if err := d.Take(asks.ChannelSubject(verb, Kind), raw); err != nil {
if err := d.Take(channel.ChannelSubject(verb, Kind), raw); err != nil {
t.Fatal(err)
}
}
@@ -74,16 +74,16 @@ func take(t *testing.T, d *Desk, verb string, m asks.Message) {
// says a click as a choice the router judges — only on a machine the message was shown on.
func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
d, shown, said, _ := newDesk(t, "laptop", "away-machine")
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "3 failed services", Body: "Needs you: <mend> them.",
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "3 failed services", Body: "Needs you: <mend> them.",
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
if len(*shown) != 1 || (*shown)[0].machine != "laptop" || (*shown)[0].args["expire_ms"] != 0 ||
(*shown)[0].args["body"] != "Needs you: &lt;mend&gt; them." {
t.Fatalf("shown %+v", *shown)
}
d.Chosen("e1", "laptop", "t1")
d.Chosen("e1", "laptop", "t1") // a redelivery
got := said[asks.IntakeSubject(asks.Chosen, Kind)]
var c asks.Choice
got := said[channel.IntakeSubject(channel.Chosen, Kind)]
var c channel.Choice
if len(got) != 1 || json.Unmarshal(got[0], &c) != nil || c.Ticket != "t1" || c.Handle != "m1" ||
c.Sender.Identity != "laptop" || c.Sender.Authenticated {
t.Fatalf("said %s", got)
@@ -91,14 +91,14 @@ func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
// Elsewhere it is refused, there, and nothing is said to the router.
d.Chosen("e2", "workstation", "t1")
d.Chosen("e3", "laptop", "forged")
if len(said[asks.IntakeSubject(asks.Chosen, Kind)]) != 1 {
if len(said[channel.IntakeSubject(channel.Chosen, Kind)]) != 1 {
t.Error("a click not shown there was said")
}
if last := (*shown)[len(*shown)-1]; last.machine != "laptop" || last.args["summary"] != "That answer was not taken" {
t.Errorf("the refusal was not shown where it was chosen: %+v", last)
}
// The router's reply to the click is shown on the machine it was made on.
take(t, d, asks.Send, asks.Message{Handle: "r1", Title: "Details: 3 failed services", Reply: c.ID})
take(t, d, channel.Send, channel.Message{Handle: "r1", Title: "Details: 3 failed services", Reply: c.ID})
if last := (*shown)[len(*shown)-1]; last.machine != "laptop" || last.args["summary"] != "Details: 3 failed services" {
t.Errorf("%+v", last)
}
@@ -107,8 +107,8 @@ func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
// The note follows the body after a blank line: the desk has no smaller text, and leaves nothing out (issue 639).
func TestTheNoteFollowsTheBody(t *testing.T) {
d, shown, _, _ := newDesk(t, "laptop")
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "q", Body: "The summary.", Note: "Asked by mesh-knowledge at 16:05.",
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "q", Body: "The summary.", Note: "Asked by mesh-knowledge at 16:05.",
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
if last := (*shown)[len(*shown)-1]; last.args["body"] != "The summary.\n\nAsked by mesh-knowledge at 16:05." {
t.Errorf("the body: %q", last.args["body"])
}
@@ -116,8 +116,8 @@ func TestTheNoteFollowsTheBody(t *testing.T) {
func TestAnEditReplacesTheNotification(t *testing.T) {
d, shown, _, _ := newDesk(t, "laptop")
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "q", Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
take(t, d, asks.Edit, asks.Message{Handle: "m1", Title: "Silence for a week: chosen"})
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "q", Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
take(t, d, channel.Edit, channel.Message{Handle: "m1", Title: "Silence for a week: chosen"})
if last := (*shown)[len(*shown)-1]; last.args["replace_id"] != 1 || last.args["actions"] != nil {
t.Errorf("the edit: %+v", last.args)
}
@@ -134,17 +134,17 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
d, shown, said, store := newDesk(t, "laptop")
d.Paused("laptop", false)
d.SayStanding()
take(t, d, asks.Show, asks.Message{Handle: "open", Title: "2 failed services on shanks",
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
take(t, d, asks.Show, asks.Message{Handle: "silenced", Title: "Agent-home shrank",
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t-silenced"}}})
take(t, d, asks.Edit, asks.Message{Handle: "silenced", Title: "Silenced until Saturday: Agent-home shrank", Silent: true})
take(t, d, asks.Show, asks.Message{Handle: "resolved", Title: "Questions for you not delivered",
Buttons: []asks.Button{{Label: "Details", Ticket: "t-resolved"}}})
take(t, d, asks.Edit, asks.Message{Handle: "resolved", Title: "Resolved: questions for you are delivered", Silent: true})
take(t, d, channel.Show, channel.Message{Handle: "open", Title: "2 failed services on shanks",
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
take(t, d, channel.Show, channel.Message{Handle: "silenced", Title: "Agent-home shrank",
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-silenced"}}})
take(t, d, channel.Edit, channel.Message{Handle: "silenced", Title: "Silenced until Saturday: Agent-home shrank", Silent: true})
take(t, d, channel.Show, channel.Message{Handle: "resolved", Title: "Questions for you not delivered",
Buttons: []channel.Button{{Label: "Details", Ticket: "t-resolved"}}})
take(t, d, channel.Edit, channel.Message{Handle: "resolved", Title: "Resolved: questions for you are delivered", Silent: true})
openID := store["open"].On["laptop"]
before := len(*shown)
standings := len(said[asks.IntakeSubject(asks.StandingWhat, Kind)])
standings := len(said[channel.IntakeSubject(channel.StandingWhat, Kind)])
later := d.Now().Add(time.Hour)
d.Now = func() time.Time { return later }
d.Paused("laptop", false)
@@ -157,25 +157,25 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
if store["open"].On["laptop"] != openID {
t.Fatalf("the open message's notification was forgotten: %+v", store["open"].On)
}
got := said[asks.IntakeSubject(asks.StandingWhat, Kind)]
got := said[channel.IntakeSubject(channel.StandingWhat, Kind)]
var s Standing
if len(got) != standings+1 || json.Unmarshal(got[len(got)-1], &s) != nil || !s.Ready || !s.AnswersLostAt.Equal(later) {
t.Fatalf("the standing after the start: %d said, last %s", len(got)-standings, got[len(got)-1])
}
// The router says the open one again under its handle: in place of its notification, with its answers.
take(t, d, asks.Show, asks.Message{Handle: "open", Title: "2 failed services on shanks",
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
take(t, d, channel.Show, channel.Message{Handle: "open", Title: "2 failed services on shanks",
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
last := (*shown)[len(*shown)-1]
if len(*shown) != before+1 || last.args["replace_id"] != openID || last.args["actions"] == nil {
t.Fatalf("the open message said again: %+v", last.args)
}
d.Chosen("e2", "laptop", "t-open")
if len(said[asks.IntakeSubject(asks.Chosen, Kind)]) != 1 {
if len(said[channel.IntakeSubject(channel.Chosen, Kind)]) != 1 {
t.Error("a click on the message said again was not said to the router")
}
// The same start event again (a redelivery) does nothing more.
standings = len(said[asks.IntakeSubject(asks.StandingWhat, Kind)])
if n := d.Started("e-start", "laptop"); n != 0 || len(said[asks.IntakeSubject(asks.StandingWhat, Kind)]) != standings {
standings = len(said[channel.IntakeSubject(channel.StandingWhat, Kind)])
if n := d.Started("e-start", "laptop"); n != 0 || len(said[channel.IntakeSubject(channel.StandingWhat, Kind)]) != standings {
t.Errorf("a redelivered start was acted on again: %d", n)
}
// A desk none of whose notifiers started again says a zero time, which the router reads as none.
@@ -187,8 +187,8 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
func TestNoMachineThatShowsItIsAFailureSaidToTheRouter(t *testing.T) {
d, _, said, _ := newDesk(t, "away-machine")
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "x"})
if f := said[asks.IntakeSubject(asks.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "no session answers") ||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "x"})
if f := said[channel.IntakeSubject(channel.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "no session answers") ||
strings.Contains(string(f[0]), `"permanent":true`) {
t.Errorf("%s", f)
}
@@ -229,7 +229,7 @@ func TestItPromisesNoVerifiedSender(t *testing.T) {
func TestASecretMessageIsShownAndNotKept(t *testing.T) {
d, shown, _, store := newDesk(t, "laptop")
d.Paused("laptop", false)
take(t, d, asks.Send, asks.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true})
take(t, d, channel.Send, channel.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true})
if len(*shown) != 1 {
t.Fatal("not shown")
}
@@ -252,8 +252,8 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
if s := d.Standing(); !s.Ready {
t.Fatalf("not ready with laptop heard unpaused: %+v", s)
}
code := asks.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}
take(t, d, asks.Send, code)
code := channel.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}
take(t, d, channel.Send, code)
if len(*shown) != 1 || (*shown)[0].machine != "laptop" || (*shown)[0].args["secret"] != true {
t.Fatalf("the code was shown as %+v", *shown)
}
@@ -262,11 +262,11 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
}
// Paused: not shown anywhere, said as failed, and the desk is not ready.
d.Paused("laptop", true)
take(t, d, asks.Send, code)
take(t, d, channel.Send, code)
if len(*shown) != 1 {
t.Fatalf("the code was shown on a paused notifier: %+v", *shown)
}
if f := said[asks.IntakeSubject(asks.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "paused") {
if f := said[channel.IntakeSubject(channel.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "paused") {
t.Errorf("not said as failed: %s", f)
}
if s := d.Standing(); s.Ready || !strings.Contains(s.Why, "paused") {
@@ -284,7 +284,7 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
t.Error("a machine not named made the desk ready")
}
// A message that is not secret still goes to every named machine, paused or not.
take(t, d, asks.Send, asks.Message{Handle: "m9", Title: "x"})
take(t, d, channel.Send, channel.Message{Handle: "m9", Title: "x"})
if len(*shown) != 3 {
t.Errorf("an ordinary message was held back: %+v", *shown)
}
@@ -330,8 +330,8 @@ func TestItRunsAsItsOwnAccount(t *testing.T) {
// was not shown; a message under a new handle replaces nothing.
func TestAMessageSaidAgainUnderItsHandleReplacesItsNotification(t *testing.T) {
d, shown, _, store := newDesk(t, "laptop", "desktop")
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "2 failed services on shanks"})
take(t, d, asks.Send, asks.Message{Handle: "m2", Title: "another problem"})
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "2 failed services on shanks"})
take(t, d, channel.Send, channel.Message{Handle: "m2", Title: "another problem"})
if len(*shown) != 4 {
t.Fatalf("shown %d", len(*shown))
}
@@ -343,8 +343,8 @@ func TestAMessageSaidAgainUnderItsHandleReplacesItsNotification(t *testing.T) {
first := store["m1"].On
// The desk now names a third machine as well.
d.Machines = func() []string { return []string{"laptop", "desktop", "third"} }
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks", Urgent: true,
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks", Urgent: true,
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
again := (*shown)[4:]
if len(again) != 3 {
t.Fatalf("said again on %d machines", len(again))
@@ -397,18 +397,18 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
d, shown, _, store := newDesk(t, "laptop")
down := true
d.Store = downStore{store, &down}
raised, _ := json.Marshal(asks.Message{Handle: "m9", Title: "A new build cannot be delivered", Urgent: true})
if err := d.Take(asks.ChannelSubject(asks.Send, Kind), raised); err != nil {
raised, _ := json.Marshal(channel.Message{Handle: "m9", Title: "A new build cannot be delivered", Urgent: true})
if err := d.Take(channel.ChannelSubject(channel.Send, Kind), raised); err != nil {
t.Fatalf("the raise was not taken: %v", err)
}
cleared, _ := json.Marshal(asks.Message{Handle: "m9", Title: "Cleared: a new build can be delivered"})
if err := d.Take(asks.ChannelSubject(asks.Edit, Kind), cleared); err != nil {
cleared, _ := json.Marshal(channel.Message{Handle: "m9", Title: "Cleared: a new build can be delivered"})
if err := d.Take(channel.ChannelSubject(channel.Edit, Kind), cleared); err != nil {
t.Fatalf("the clearing was not taken: %v", err)
}
d.KeepUnkept() // the minute's standing, store still down
down = !back
d.KeepUnkept()
take(t, d, asks.Send, asks.Message{Handle: "m10", Title: "another"})
take(t, d, channel.Send, channel.Message{Handle: "m10", Title: "another"})
if len(*shown) != 3 || (*shown)[1].args["replace_id"] != 1 || (*shown)[1].args["summary"] != "Cleared: a new build can be delivered" {
t.Fatalf("the clearing did not replace the urgent notification: %+v", *shown)
}
@@ -423,7 +423,7 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
}
d, shown, said, store := newDesk(t, "laptop", "desktop")
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "2 failed services on shanks"})
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "2 failed services on shanks"})
first := store["m1"].On
notify := d.Notify
d.Notify = func(machine string, args map[string]any) (json.RawMessage, error) {
@@ -432,13 +432,13 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
}
return notify(machine, args)
}
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks"})
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks"})
if got := store["m1"].On["desktop"]; got != first["desktop"] {
t.Fatalf("the desktop's notification %d was forgotten: %v", first["desktop"], store["m1"].On)
}
d.Notify = func(string, map[string]any) (json.RawMessage, error) { return nil, errors.New("away") }
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "Now urgent: 2 failed services on shanks"})
if len(said[asks.IntakeSubject(asks.FailedWhat, Kind)]) != 1 {
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "Now urgent: 2 failed services on shanks"})
if len(said[channel.IntakeSubject(channel.FailedWhat, Kind)]) != 1 {
t.Fatalf("shown nowhere, and not said as failed")
}
_ = shown
+2 -2
View File
@@ -2,10 +2,10 @@ module deskchannel
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+6 -5
View File
@@ -3,20 +3,21 @@ module diskload
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats.go v1.54.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // indirect
github.com/nats-io/nats-server/v2 v2.11.17 // 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
golang.org/x/time v0.15.0 // indirect
golang.org/x/time v0.16.0 // indirect
)
+12 -10
View File
@@ -1,15 +1,17 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
@@ -23,5 +25,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
@@ -59,7 +59,7 @@ func tagsAnswer(v *holder.Verbs, a holder.VersionsAnswer) reg.TagsAnswer {
return out
}
// eventBody is an event's body as the seat's contract defines it: an Outcome for published, refused and removed, a
// eventBody is an event's body as the seat's contract defines it: an Outcome for published, refused, removed and kept, a
// Hold for held and released.
func eventBody(event string, e holder.Event) any {
switch event {
@@ -67,5 +67,5 @@ func eventBody(event string, e holder.Event) any {
return reg.Hold{Name: e.Name, Digest: e.Digest, Holder: e.Holder, By: e.By, Reason: e.Reason, At: time.Now().UTC().Format(time.RFC3339)}
}
return reg.Outcome{Name: e.Name, Version: e.Version, Kind: e.Kind, Tag: e.Tag, Digest: e.Digest, Fingerprint: e.Fingerprint,
Commit: e.Commit, Holder: e.Holder, Transfer: e.Transfer, Reference: e.Reference, Reason: e.Reason}
Commit: e.Commit, Holder: e.Holder, Transfer: e.Transfer, Reference: e.Reference, Reason: e.Reason, Tags: e.Tags}
}
@@ -49,7 +49,7 @@ func TestTheVerbsSpeakTheSeatsContract(t *testing.T) {
if seen != 14 {
t.Errorf("%d seat verbs served, want 14 (8 on the image seat, 6 on the artifact seat)", seen)
}
for event, want := range map[string]any{holder.EventPublished: &reg.Outcome{}, holder.EventHeld: &reg.Hold{}, holder.EventReleased: &reg.Hold{}} {
for event, want := range map[string]any{holder.EventPublished: &reg.Outcome{}, holder.EventKept: &reg.KeptEvent{}, holder.EventHeld: &reg.Hold{}, holder.EventReleased: &reg.Hold{}} {
b, _ := json.Marshal(eventBody(event, holder.Event{Name: "m/a", Kind: holder.KindArchive, Digest: "sha256:aa", Holder: holder.Self, By: "x.artifact-store-tools"}))
dec := json.NewDecoder(bytes.NewReader(b))
dec.DisallowUnknownFields()
@@ -20,6 +20,7 @@ import (
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/parts"
@@ -80,27 +81,6 @@ func finding(kind, subject, words string, raised bool) {
}
}
// closeBootstrap asks the program to discard the bootstrap's one-off credential once a publish is answered (design 51
// *On a fresh mesh*): the window bootstrap-push closed in the state windows, which the program follows and then removes
// the file. The tools bundle runs in the tool runner's account, which cannot remove a file in distribution's state
// directory. Once per process; the window's record says it to the next.
var closeBootstrap func()
func bootstrapCloser(windows kept[holder.Window]) func() {
return sync.OnceFunc(func() {
cur, _, err := windows.Get(holder.BootstrapWindow)
if err == nil && !cur.Closed.IsZero() {
return
}
w := holder.Window{Name: holder.BootstrapWindow, Closed: time.Now().UTC()}
if err := windows.Put(w); err != nil {
logf("the bootstrap's one-off credential could not be asked away: %v", err)
return
}
logf("the bootstrap's one-off credential is asked away: the holders serve publish")
})
}
// learned are the proved callers whose build this process recorded as publishing through the seats.
var learned sync.Map
@@ -185,6 +165,12 @@ func learnInto(state casState, module, fp string, attempts int) (bool, error) {
return false, fmt.Errorf("%s was written by another writer first %d times: %w", holder.PublishingKey, attempts, stdio.ErrChanged)
}
// recordAct records a hand act through the seat mesh-hand-acts alone.
func recordAct(q handacts.Request) error {
_, err := handacts.Recorder{Ask: stdio.Ask}.Record(q)
return err
}
// mirrors is the state mirrors: an ask is created once; one already kept is the same copy, taken on already.
type mirrors struct{}
@@ -202,6 +188,28 @@ func (mirrors) Claim(key string, r holder.MirrorRequest, revision uint64) (bool,
return err == nil, err
}
func (mirrors) Delete(key string) error { return stdio.State(holder.MirrorsState).Delete(key) }
func (mirrors) List() (map[string]holder.MirrorRequest, error) {
keys, err := stdio.State(holder.MirrorsState).Keys()
if err != nil {
return nil, err
}
out := map[string]holder.MirrorRequest{}
for _, k := range keys {
e, err := stdio.State(holder.MirrorsState).Get(k)
if err != nil {
return nil, err
}
var r holder.MirrorRequest
if e != nil && json.Unmarshal(e.Value, &r) == nil {
out[k] = r
}
}
return out, nil
}
func (mirrors) Put(key string, r holder.MirrorRequest) error {
_, err := stdio.State(holder.MirrorsState).Put(key, r)
return err
}
// followMirrors runs every copy `mirror` took on, on the state's watch: the asks kept when the bundle starts (a copy a
// restart cut) and each new one, one at a time. Each is claimed first (Verbs.Take), so a second follower copies none
@@ -266,11 +274,7 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
if err != nil {
return nil, err
}
_, _ = v.DropIdle()
a, err := v.Publish(seat, stdio.CallerOf(args), publishOf(in))
if err == nil && closeBootstrap != nil {
closeBootstrap()
}
if err == nil {
learnPublishing(windows, stdio.CallerOf(args))
}
@@ -318,10 +322,11 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
return nil, err
}
r := holder.RemoveRequest{Name: in.Name, Version: in.Version, Digest: in.Digest, Reason: in.Reason}
if err := v.Remove(seat, stdio.CallerOf(args), r); err != nil {
got, err := v.Remove(seat, stdio.CallerOf(args), r)
if err != nil {
return nil, err
}
return reg.RemoveAnswer{Removed: true, Artifact: &reg.Artifact{Name: in.Name, Version: in.Version, Tag: in.Tag, Digest: in.Digest}}, nil
return reg.RemoveAnswer{Removed: got.Removed, Kept: got.Kept, Reason: got.Reason, Tags: got.Tags, Artifact: &reg.Artifact{Name: in.Name, Version: in.Version, Tag: in.Tag, Digest: in.Digest}}, nil
}},
stdio.Tool{Name: seat + "." + holder.VerbHold, Description: "Keep one untagged bundle, archive or copied base from retention (the artifact sweep alone); says held.",
Input: obj(map[string]any{"name": s("name"), "digest": s("sha256:…"), "reason": s("which of the sweep's rules keeps it")}, "name", "digest", "reason"),
@@ -412,16 +417,42 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
}
idle, _ := v.DropIdle()
if id := str(args, "drop"); id != "" {
if str(args, "why") == "" {
return nil, errors.New("a drop is a hand act: say why")
t, err := v.DropByHand(id, stdio.CallerOf(args), str(args, "why"))
if err != nil {
return nil, err
}
t, err := v.Drop(id)
logf("transfer %s dropped by hand by %s: %s", id, stdio.CallerOf(args), str(args, "why"))
return map[string]any{"dropped": t.ID}, err
return map[string]any{"dropped": t.ID}, nil
}
all, err := v.Transfers.List()
used, _ := v.Objects.Used()
return map[string]any{"transfers": all, "used": used, "cap": holder.IncomingCap, "dropped_idle": idle}, err
return map[string]any{"transfers": all, "used": used, "cap": v.CapNow(), "dropped_idle": idle}, err
}},
stdio.Tool{Name: "registry_mirrors", Description: "The copies of outside images mirror took on and has not ended (the state mirrors): each by its key, its source, digest, who asked and the follower that claimed it (none: it waits). With again, asks one again by hand, its claim let go so a follower takes it on; with drop, drops one by hand, saying refused on the image seat's events so its waiter ends (each a hand act, why required).",
Input: obj(map[string]any{"again": s("a kept copy's key to ask again"), "drop": s("a kept copy's key to drop"), "why": s("why (a hand act)")}),
Run: func(args map[string]any) (any, error) {
by, why := stdio.CallerOf(args), str(args, "why")
switch {
case str(args, "again") != "":
if why == "" {
return nil, errors.New("asking a copy again is a hand act: say why")
}
r, err := v.MirrorAgain(str(args, "again"))
if err != nil {
return nil, err
}
logf("the copy %s asked again by hand by %s: %s", str(args, "again"), by, why)
return map[string]any{"asked_again": str(args, "again"), "copy": r}, nil
case str(args, "drop") != "":
r, err := v.MirrorDrop(str(args, "drop"), by, why)
if err != nil {
return nil, err
}
logf("the copy %s dropped by hand by %s: %s", str(args, "drop"), by, why)
return map[string]any{"dropped": str(args, "drop"), "copy": r}, nil
}
all, err := v.MirrorsNow()
return map[string]any{"mirrors": all}, err
}},
stdio.Tool{Name: "registry_holds", Description: "What is held, since when and for whom; with release, lets one go through the seat's release as the operator's session (a hand act).",
Input: obj(map[string]any{"seat": s("the seat"), "release": s("the name to release"), "digest": s("its digest"), "why": s("why")}),
@@ -465,12 +496,7 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
return nil, err
}
}
b, _, err := windows.Get(holder.BootstrapWindow)
if err != nil {
return nil, err
}
// The program removes the file when this window closes and writes it closed again with its removal's time.
return map[string]any{"window": cur, "bootstrap_credential_discarded": !b.Closed.IsZero(), "bootstrap": b}, nil
return map[string]any{"window": cur}, nil
}})
}
return out
@@ -480,11 +506,17 @@ func main() {
base, _ := url.Parse("http://127.0.0.1:5001")
store := &holder.OCI{Base: base, Upstream: &holder.Upstream{}}
h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: delivery.BuilderVersions{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding,
Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState)}
Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Record: recordAct, Idle: parts.BusArm(holder.IdleState)}
v := &holder.Verbs{Holder: h, Catalog: store, Holds: kept[holder.Hold]{state: "holds", key: holder.Hold.Key}, Mirrors: mirrors{}}
go followMirrors(v)
go parts.FollowIdle(holder.IdleState, parts.IDs(h.Transfers), h.Expired, func(id string, dropped bool, err error) {
if err != nil {
logf("%v", err)
return
}
logf("transfer %s had no part for %s: dropped with its parts", id, holder.TransferIdle)
})
windows := kept[holder.Window]{state: "windows", key: func(w holder.Window) string { return w.Name }}
closeBootstrap = bootstrapCloser(windows)
if err := stdio.Serve("", tools(v, windows, holder.ConfiguredCallers)); err != nil {
logf("%v", err)
os.Exit(1)
+1 -46
View File
@@ -1,7 +1,7 @@
// Command distribution is distribution's program (novox/hq design 51): it serves the artifact store's port in front of
// the registry container, which listens on its node's loopback only. Reads (GET, HEAD) go through to every node's
// container runtime; a write is refused but while the direct-write window is open, never over a tag the seat holds
// published, or with the bootstrap's one-off credential (holder.Front). The windows are distribution's state windows:
// published (holder.Front). The windows are distribution's state windows:
// the program moves them (windows.go) and follows the direct-write window's record on each change; it holds no timer.
package main
@@ -10,7 +10,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"net/http"
"net/url"
@@ -58,7 +57,6 @@ func run(ctx context.Context, getenv func(string) string) error {
windows := process.OptionalBucket(ctx, js, "distribution_windows", "the direct-write window is read as open until the program starts again", logger.Printf)
if windows != nil {
go follow(ctx, windows, &open, logger)
go followBootstrap(ctx, windows, logger)
calls, err := process.Connect(cred)
if err != nil {
return err
@@ -72,13 +70,6 @@ func run(ctx context.Context, getenv func(string) string) error {
Published: func(name, tag string) (bool, error) {
d, err := oci.Digest(name, tag)
return d != "", err
},
Bootstrap: func() (string, bool) {
b, err := os.ReadFile(holder.BootstrapFile)
if err != nil {
return "", false
}
return strings.TrimSpace(string(b)), true
}}
srv := &http.Server{Addr: or(getenv("MESH_REGISTRY_LISTEN"), ":5000"), Handler: front, ReadHeaderTimeout: 30 * time.Second}
errs := make(chan error, 1)
@@ -119,42 +110,6 @@ func follow(ctx context.Context, kv jetstream.KeyValue, open *atomic.Bool, logge
}
}
// followBootstrap removes the bootstrap's one-off credential (holder.BootstrapFile) when the tools bundle closes the
// window bootstrap-push at its first answered publish (design 51 *On a fresh mesh*): the program runs as distribution's
// account, which owns the file. A file already gone is no fault; one that cannot be removed is said each time the
// window is written.
func followBootstrap(ctx context.Context, kv jetstream.KeyValue, logger *log.Logger) {
w, err := kv.Watch(ctx, holder.BootstrapWindow)
if err != nil {
logger.Printf("the bootstrap's credential window cannot be watched (%v): its file stays until the program starts again", err)
return
}
for e := range w.Updates() {
if e == nil || e.Operation() != jetstream.KeyValuePut {
continue
}
var win holder.Window
if json.Unmarshal(e.Value(), &win) != nil || win.Closed.IsZero() {
continue
}
if why := removeBootstrap(holder.BootstrapFile); why != "" {
logger.Printf("%s", why)
}
}
}
// removeBootstrap removes the credential's file; it answers what went wrong, or "" when the file is gone.
func removeBootstrap(path string) string {
switch err := os.Remove(path); {
case err == nil:
return "the bootstrap's one-off credential is discarded: the holders serve publish"
case errors.Is(err, fs.ErrNotExist):
return ""
default:
return fmt.Sprintf("the bootstrap's one-off credential could not be discarded, so it is still accepted: %v", err)
}
}
func or(v, d string) string {
if strings.TrimSpace(v) == "" {
return d
@@ -1,35 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// The program, which runs as distribution's account, removes the bootstrap's one-off credential when its window
// closes (design 51 *On a fresh mesh*; a3-publish-contract): removed once, a file already gone is no fault, and one
// that cannot be removed is said, since the front still accepts it.
func TestTheProgramDiscardsTheBootstrapCredential(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "bootstrap-push")
if err := os.WriteFile(file, []byte("token\n"), 0o600); err != nil {
t.Fatal(err)
}
if why := removeBootstrap(file); !strings.Contains(why, "discarded") {
t.Fatalf("first removal = %q, want it said discarded", why)
}
if _, err := os.Stat(file); !os.IsNotExist(err) {
t.Fatalf("the file is still there: %v", err)
}
if why := removeBootstrap(file); why != "" {
t.Fatalf("a file already gone = %q, want nothing said", why)
}
stuck := filepath.Join(dir, "stuck")
if err := os.MkdirAll(filepath.Join(stuck, "child"), 0o700); err != nil {
t.Fatal(err)
}
if why := removeBootstrap(stuck); !strings.Contains(why, "still accepted") {
t.Fatalf("an unremovable file = %q, want it said still accepted", why)
}
}
@@ -91,9 +91,12 @@ func (d busDelivery) Reads(node string) (bool, []string, error) {
raw, err := d.c.CallOn(node, readsAsk, map[string]any{})
if err != nil {
if notServed(err) {
return false, nil, nil // a node-engine not serving manifest-reads states nothing (design 51)
// A node-engine not serving manifest-reads states nothing (design 51); what the call answered (an away node,
// an older node-engine, a timeout) goes with it, so the window's tool says why the node holds it.
return false, nil, err
}
return false, nil, err
// Any other failure is a read that did not happen: served true with its error, so the wake judges nothing.
return true, nil, err
}
var a struct {
Reads []string `json:"manifest-reads"`
+7 -6
View File
@@ -3,21 +3,22 @@ module distribution
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats.go v1.54.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // indirect
github.com/nats-io/nats-server/v2 v2.11.17 // 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
golang.org/x/time v0.15.0 // indirect
golang.org/x/time v0.16.0 // indirect
)
+14 -10
View File
@@ -1,15 +1,19 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/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=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639 h1:5HeZzk6tKzcSwVivD3IcIvQ/2WIvysZvNvNKyq/oIBA=
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
@@ -23,5 +27,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
+27 -6
View File
@@ -3,6 +3,7 @@ package holder
import (
"errors"
"fmt"
"maps"
"slices"
"sort"
"strings"
@@ -44,7 +45,8 @@ type NodeHave struct {
// Delivery is what the windows' driver asks of the delivery parts and the node-engine seat, through their tools and
// verbs on the bus (ADR 0596): every node's haves, away ones included, and one node-engine's stated reader
// capabilities. Reads answers served false when the node-engine does not serve `manifest-reads` (it states nothing).
// capabilities. Reads answers served false when the node-engine does not serve `manifest-reads` (it states nothing),
// with the call's error, if any, saying why (an away node, an older node-engine, a timeout).
type Delivery interface {
Haves() ([]NodeHave, error)
Reads(node string) (served bool, stated []string, err error)
@@ -118,6 +120,9 @@ func (d *Driver) Read() ([]NodeRead, error) {
return nil, fmt.Errorf("%s's manifest-reads cannot be read, so no window moves: %w", node, err)
}
n.Serves, n.Stated = served, stated
if !served && err != nil {
n.NotServed = err.Error()
}
out = append(out, *n)
}
return out, nil
@@ -131,13 +136,20 @@ func (d *Driver) Wake(why string) ([]string, error) {
return nil, err
}
now := d.now()
named := make([]string, 0, len(nodes))
byNode := map[string]NodeRead{}
for _, n := range nodes {
named = append(named, n.Node)
byNode[n.Node] = n
}
var said, errs []string
for _, c := range []struct {
name, event, seat string
holding func([]NodeRead, []DroppedNode) []string
why func(NodeRead) string
}{
{OldNameWindow, OldNameClosedEvent, OldSeat, HoldingOldName},
{DirectWritesWindow, DirectWritesClosedEvent, "", HoldingDirectWrites},
{OldNameWindow, OldNameClosedEvent, OldSeat, HoldingOldName, WhyOldName},
{DirectWritesWindow, DirectWritesClosedEvent, "", HoldingDirectWrites, WhyDirectWrites},
} {
cur, err := d.State.Window(c.name)
if err != nil {
@@ -145,9 +157,16 @@ func (d *Driver) Wake(why string) ([]string, error) {
continue
}
if cur.Name == "" {
cur = Window{Name: c.name, Open: true}
cur = Window{Name: c.name}
cur.Open = true
}
next, closed, overdue := cur.Step(named, c.holding(nodes, cur.Dropped), now)
if next.Open && len(named) > 0 {
next.Why = map[string]string{}
for _, n := range next.Holding {
next.Why[n] = c.why(byNode[n])
}
}
next, closed, overdue := cur.Step(c.holding(nodes, cur.Dropped), now)
if !windowsEqual(cur, next) {
if err := d.State.PutWindow(next); err != nil {
errs = append(errs, fmt.Sprintf("the %s window is not kept: %v", c.name, err))
@@ -172,6 +191,8 @@ func (d *Driver) Wake(why string) ([]string, error) {
}
}
said = append(said, fmt.Sprintf("the %s window is closed (%s); it was held last by %v", c.name, why, closed.LastHolding))
} else if len(named) == 0 {
said = append(said, fmt.Sprintf("the %s window is not judged (%s): the read named no node", c.name, why))
} else if next.Open {
said = append(said, fmt.Sprintf("the %s window stays open (%s): held by %v", c.name, why, next.Holding))
}
@@ -191,5 +212,5 @@ func (d *Driver) now() time.Time {
func windowsEqual(a, b Window) bool {
return a.Open == b.Open && slices.Equal(a.Holding, b.Holding) && a.Closed.Equal(b.Closed) &&
len(a.Since) == len(b.Since) && len(a.Dropped) == len(b.Dropped)
len(a.Since) == len(b.Since) && len(a.Dropped) == len(b.Dropped) && maps.Equal(a.Why, b.Why)
}
+4 -23
View File
@@ -1,7 +1,6 @@
package holder
import (
"crypto/subtle"
"fmt"
"net/http"
"net/http/httputil"
@@ -15,7 +14,8 @@ import (
// protocol's GET and HEAD) stay open to every node's container runtime; a write reaches the store only from
// distribution's own process, which writes to the loopback directly and never through the front. While the direct-write
// window is open, a build module's old build still pushes through the front as today, but never to a tag the seat holds
// published.
// published. No credential opens it: on a fresh mesh the bootstrap pushes the holders' own images to the store on its
// loopback, never through the front (design 51 *On a fresh mesh*).
type Front struct {
// Store is the registry container's loopback address.
Store *url.URL
@@ -23,9 +23,6 @@ type Front struct {
Window *atomic.Bool
// Published says whether name holds a tag published through the seat.
Published func(name, tag string) (bool, error)
// Bootstrap is the bootstrap's one-off credential for the holders' first images (design 51 *On a fresh mesh*):
// read on each write, never kept, so the file's removal (BootstrapFile) refuses the next write that carries it.
Bootstrap func() (token string, ok bool)
Logf func(string, ...any)
proxy *httputil.ReverseProxy
}
@@ -40,14 +37,7 @@ func (f *Front) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.proxy.ServeHTTP(w, r)
return
}
bootstrap := false
if f.Bootstrap != nil {
if token, ok := f.Bootstrap(); ok && token != "" {
got, has := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
bootstrap = has && subtle.ConstantTimeCompare([]byte(got), []byte(token)) == 1
}
}
if !bootstrap && (f.Window == nil || !f.Window.Load()) {
if f.Window == nil || !f.Window.Load() {
refuse(w, http.StatusForbidden, "DENIED", "the store takes writes from distribution's own process alone: publish through the seat's publish")
return
}
@@ -62,21 +52,12 @@ func (f *Front) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
}
if bootstrap {
if f.Logf != nil {
f.Logf("a write %s %s from %s with the bootstrap's one-off credential", r.Method, r.URL.Path, r.RemoteAddr)
}
} else if f.Logf != nil {
if f.Logf != nil {
f.Logf("a direct write %s %s from %s, accepted while the direct-write window is open", r.Method, r.URL.Path, r.RemoteAddr)
}
f.proxy.ServeHTTP(w, r)
}
// BootstrapFile is where the node-engine's bootstrap gives distribution its one-off credential, in distribution's
// state directory (0600, distribution's account): one line, the token. The tools bundle removes it when it answers its
// first publish, so the holders serve publish and the credential is gone (RemoveBootstrap).
const BootstrapFile = "/var/lib/mesh/registry/bootstrap-push"
// manifestPut is a PUT of /v2/<name>/manifests/<tag>, a tag and not a digest.
func manifestPut(r *http.Request) (name, tag string, ok bool) {
if r.Method != http.MethodPut {
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -16,6 +17,7 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/handacts"
)
type memObjects map[string][]byte
@@ -89,6 +91,7 @@ type fakeCatalog struct {
tags map[string]string // name:version -> digest
removed []string
mirrorErr error
tagErr error
indexes map[string][]string // name@digest -> the platform manifests it names
}
@@ -104,11 +107,27 @@ func (c *fakeCatalog) Versions(name string) ([]string, error) {
func (c *fakeCatalog) Digest(name, version string) (string, error) {
return c.tags[name+":"+version], nil
}
// Remove deletes by the digest, as the store does: every tag naming the digest goes with it.
func (c *fakeCatalog) Remove(name, version string) error {
c.removed = append(c.removed, name+":"+version)
delete(c.tags, name+":"+version)
d := c.tags[name+":"+version]
for k, v := range c.tags {
if n, _, _ := strings.Cut(k, ":"); n == name && v == d {
delete(c.tags, k)
}
}
return nil
}
// RemoveTag answers as OCI.RemoveTag over registry 2.8.3 does (fakeRegistry's DELETE by tag): the store cannot
// remove a tag alone, and nothing goes. tagErr is any other failure of the store.
func (c *fakeCatalog) RemoveTag(name, version string) error {
if c.tagErr != nil {
return c.tagErr
}
return fmt.Errorf("%w: DELETE of %s:%s by its tag answered 400 DIGEST_INVALID", ErrNoTagAlone, name, version)
}
func (c *fakeCatalog) RemoveDigest(name, digest string) error {
c.removed = append(c.removed, name+"@"+digest)
return nil
@@ -143,6 +162,14 @@ func (m memMirrors) Claim(k string, r MirrorRequest, rev uint64) (bool, error) {
return true, nil
}
func (m memMirrors) Delete(k string) error { delete(m, k); return nil }
func (m memMirrors) List() (map[string]MirrorRequest, error) {
out := map[string]MirrorRequest{}
for k, r := range m {
out[k] = r
}
return out, nil
}
func (m memMirrors) Put(k string, r MirrorRequest) error { m[k] = r; return nil }
// mirrorRev is a kept ask's revision in memMirrors: 1 unclaimed, 2 once claimed.
func mirrorRev(r MirrorRequest) uint64 {
@@ -178,6 +205,9 @@ func newBed(builderOK bool) *bed {
return b
}
// capOf is a binding's cap of n bytes.
func capOf(n uint64) func() (uint64, error) { return func() (uint64, error) { return n, nil } }
func keys[V any](m map[string]V) []string {
var out []string
for k := range m {
@@ -251,7 +281,11 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
_, err := b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t", Digest: part.Digest, Data: make([]byte, MaxPart+1)})
return err
}, "over the"},
{"a new transfer at the cap", func(b *bed) error { b.h.Cap = 100; _, err := b.h.Publish(ArtifactSeat, builder, req("t")); return err }, "at their cap"},
{"a new transfer at the cap", func(b *bed) error {
b.h.Cap = capOf(100)
_, err := b.h.Publish(ArtifactSeat, builder, req("t"))
return err
}, "at their cap"},
{"a version the builder did not number", func(b *bed) error {
b.h.Builder = fakeBuilder{ok: false}
b.store.has[part.Digest] = true // held already: the publish ends at once
@@ -265,14 +299,17 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
return err
}, "holds bundles and archives"},
{"remove of a version with no tag", func(b *bed) error {
return b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Version: "0.1.4", Reason: "branch ended"})
_, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Version: "0.1.4", Reason: "branch ended"})
return err
}, "has no tag"},
{"a retention removal from another caller than the sweep", func(b *bed) error {
return b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
_, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
return err
}, "artifact sweep's alone"},
{"a retention removal of a held archive", func(b *bed) error {
_ = b.v.HoldOne(ArtifactSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/a", Kind: KindArchive, Digest: part.Digest})
return b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/a", Digest: part.Digest, Reason: ReasonRetention})
_, err := b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/a", Digest: part.Digest, Reason: ReasonRetention})
return err
}, "is held"},
{"a hold of an image", func(b *bed) error {
return b.v.HoldOne(ImageSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/i", Kind: KindImage, Digest: part.Digest})
@@ -316,10 +353,55 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
if dropped, _ := b.h.DropIdle(); !reflect.DeepEqual(dropped, []string{"t2"}) {
t.Errorf("DropIdle = %v, want t2 after 24 hours with no part", dropped)
}
// Running review F2, ADR 0611 §3: the drop follows each transfer's idle key. The open arms it for 24 hours; its
// end re-arms a transfer that took a part since for the rest, and drops one idle 24 hours, its chunks with it.
b = newBed(true)
armed := map[string]time.Duration{}
b.h.Idle = func(id string, ttl time.Duration) error { armed[id] = ttl; return nil }
if _, err := b.h.Publish(ArtifactSeat, builder, req("t4")); err != nil || armed["t4"] != TransferIdle {
t.Fatalf("publish = %v, armed %v; want t4's idle key for %s", err, armed, TransferIdle)
}
b.now = b.now.Add(time.Hour)
_, _ = b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t4", Digest: part.Digest, Data: blob[:100]})
b.now = b.now.Add(TransferIdle - time.Hour)
if dropped, err := b.h.Expired("t4"); err != nil || dropped || armed["t4"] != time.Hour {
t.Errorf("the key's end an hour after a part: dropped %v, %v, armed %v; want t4 kept and armed for 1h", dropped, err, armed)
}
b.now = b.now.Add(time.Hour)
if dropped, err := b.h.Expired("t4"); err != nil || !dropped {
t.Errorf("the key's end 24 hours after the last part: dropped %v, %v; want t4 dropped", dropped, err)
}
if _, found, _ := b.h.Transfers.Get("t4"); found || len(b.h.Objects.(memObjects)) != 0 {
t.Errorf("t4 after its drop: kept %v, objects %v; want both gone", found, b.h.Objects)
}
if dropped, err := b.h.Expired("t4"); err != nil || dropped {
t.Errorf("the end of a key whose transfer is gone: %v, %v; want nothing done", dropped, err)
}
b.h.Idle = func(string, time.Duration) error { return errors.New("bus down") }
if _, err := b.h.Publish(ArtifactSeat, builder, req("t5")); err == nil || !strings.Contains(err.Error(), "idle bound") {
t.Errorf("a publish whose idle key is not written = %v; want it failed loudly", err)
}
// Seams note 2: registry_incoming's drop is a hand act recorded through mesh-hand-acts before anything goes;
// an act not recorded leaves the transfer open.
var acts []handacts.Request
b.h.Record = func(q handacts.Request) error { return errors.New("mesh-hand-acts has no holder") }
if _, err := b.h.DropByHand("t5", "person.jochen", "stuck"); err == nil || !strings.Contains(err.Error(), "stays open") {
t.Errorf("a drop whose act is not recorded = %v; want refused, t5 open", err)
}
b.h.Record = func(q handacts.Request) error { acts = append(acts, q); return nil }
if _, err := b.h.DropByHand("t5", "person.jochen", ""); err == nil || len(acts) != 0 {
t.Errorf("a drop with no why = %v, acts %v; want refused, nothing recorded", err, acts)
}
if got, err := b.h.DropByHand("t5", "person.jochen", "stuck"); err != nil || got.ID != "t5" || len(acts) != 1 || acts[0].Why != "stuck" || acts[0].Cause != "registry_incoming" || !strings.Contains(acts[0].What, "t5") || !strings.Contains(acts[0].What, "person.jochen") {
t.Errorf("a drop by hand = %+v, %v, acts %+v; want t5 dropped and its act recorded", got, err, acts)
}
if _, found, _ := b.h.Transfers.Get("t5"); found {
t.Errorf("t5 kept after its drop by hand")
}
})
t.Run("incoming's finding at 80 %, cleared below", func(t *testing.T) {
b := newBed(true)
b.h.Cap = 3500
b.h.Cap = capOf(3500)
_, _ = b.h.Publish(ArtifactSeat, builder, req("t3"))
_, _ = b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t3", Digest: part.Digest, Data: blob[:2800]})
if !b.findings["incoming-full incoming"] {
@@ -330,12 +412,12 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Error("the finding stayed after the transfer was dropped")
}
// A refusal for want of room stays loud on incoming until the holder takes a publish again.
b.h.Cap = 100
b.h.Cap = capOf(100)
_, _ = b.h.Publish(ArtifactSeat, builder, req("t4"))
if !b.findings["publish-refused incoming"] {
t.Errorf("a refusal at the cap raised %v, want publish-refused on incoming", b.findings)
}
b.h.Cap = 1 << 20
b.h.Cap = capOf(1 << 20)
b.store.has[part.Digest] = true
if _, err := b.h.Publish(ArtifactSeat, builder, req("t5")); err != nil || b.findings["publish-refused incoming"] {
t.Errorf("the next publish taken = %v, findings %v: want the refusal at the cap cleared", err, b.findings)
@@ -344,7 +426,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Run("the sweep's removal, hold and release", func(t *testing.T) {
b := newBed(true)
sweep := "novox.artifact-store-tools"
if err := b.v.Remove(ArtifactSeat, sweep, RemoveRequest{Name: "m/a", Digest: part.Digest, Kind: KindArchive, Reason: ReasonRetention}); err != nil {
if _, err := b.v.Remove(ArtifactSeat, sweep, RemoveRequest{Name: "m/a", Digest: part.Digest, Kind: KindArchive, Reason: ReasonRetention}); err != nil {
t.Fatal(err)
}
if err := b.v.HoldOne(ArtifactSeat, sweep, HoldRequest{Name: "m/b", Kind: KindArchive, Digest: part.Digest}); err != nil {
@@ -365,9 +447,50 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Errorf("said %v, want %v", events, want)
}
b.cat.tags["m/p:0.1.5-feat-x.1"] = part.Digest
if err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-x.1", Reason: "branch merged"}); err != nil || b.cat.tags["m/p:0.1.5-feat-x.1"] != "" {
if _, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-x.1", Reason: "branch merged"}); err != nil || b.cat.tags["m/p:0.1.5-feat-x.1"] != "" {
t.Errorf("the builder's removal of a tagged version: %v", err)
}
// Security review F1 on registry 2.8.3: a reproducible build gives a branch's tagged version and the trunk's
// numbered one one digest; the store cannot remove the tag alone, so the version is kept whole, the numbered one
// stays pullable, and remove answers and says kept (shared-digest, the other tags): no refusal, so the builder
// raises no retry.
b.cat.tags["m/p:0.1.6-feat-y.1"], b.cat.tags["m/p:0.1.6"] = part.Digest, part.Digest
got, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.6-feat-y.1", Reason: "branch merged"})
if err != nil || got.Removed || !got.Kept || got.Reason != ReasonSharedDigest || !reflect.DeepEqual(got.Tags, []string{"0.1.6"}) {
t.Fatalf("the removal of a tagged version sharing its digest = %+v, %v; want kept, shared-digest, tags [0.1.6], no error", got, err)
}
if b.cat.tags["m/p:0.1.6-feat-y.1"] != part.Digest || b.cat.tags["m/p:0.1.6"] != part.Digest {
t.Errorf("tags after the tagged version's removal: %v; want both kept at %s", b.cat.tags, part.Digest)
}
if last := b.said[len(b.said)-1]; last.event != EventKept || last.body.Reason != ReasonSharedDigest || last.body.Digest != part.Digest || !reflect.DeepEqual(last.body.Tags, []string{"0.1.6"}) {
t.Errorf("said %s %+v; want kept, shared-digest, its digest and the tags [0.1.6]", last.event, last.body)
}
if got, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-v.1", Reason: "branch merged"}); err != nil || !got.Removed || got.Kept {
t.Errorf("the removal of a version already gone = %+v, %v; want removed", got, err)
}
// Any other failure of the store removes nothing and is refused naming the shared digest.
b.cat.tags["m/p:0.1.7-feat-z.1"], b.cat.tags["m/p:0.1.7"] = part.Digest, part.Digest
b.cat.tagErr = errors.New("the store answered 500")
said := len(b.said)
if _, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.7-feat-z.1", Reason: "branch merged"}); err == nil || !strings.Contains(err.Error(), "shares its digest") || b.cat.tags["m/p:0.1.7"] != part.Digest || len(b.said) != said {
t.Errorf("a store failing otherwise: %v, tags %v; want refused, 0.1.7 kept, nothing said", err, b.cat.tags)
}
b.cat.tagErr = nil
// The store's own answers, as registry 2.8.3 gives them: DELETE by tag removes nothing (ErrNoTagAlone); by
// digest the manifest goes with every tag naming it.
r283 := newFakeRegistry()
sts := httptest.NewServer(r283)
defer sts.Close()
u, _ := url.Parse(sts.URL)
o := &OCI{Base: u}
manifest := []byte(`{"schemaVersion":2}`)
r283.manifests["m/p:0.1.8-feat-w.1"], r283.manifests["m/p:0.1.8"] = manifest, manifest
if err := o.RemoveTag("m/p", "0.1.8-feat-w.1"); !errors.Is(err, ErrNoTagAlone) || r283.manifests["m/p:0.1.8-feat-w.1"] == nil {
t.Errorf("RemoveTag on 2.8.3 = %v; want ErrNoTagAlone and the tag kept", err)
}
if err := o.Remove("m/p", "0.1.8"); err != nil || len(r283.manifests) != 0 {
t.Errorf("Remove by digest on 2.8.3 = %v, %d manifests left; want both tags gone", err, len(r283.manifests))
}
})
// CAT-AS1: what the sweep did over the store's door is the holder's: a removal for retention of an index removes
// its platform manifests first, but those a tagged or held index of the repository names; a manifest such an index
@@ -398,7 +521,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Fatal(err)
}
}
err := b.v.Remove(ImageSeat, sweep, RemoveRequest{Name: name, Digest: c.remove, Reason: ReasonRetention})
_, err := b.v.Remove(ImageSeat, sweep, RemoveRequest{Name: name, Digest: c.remove, Reason: ReasonRetention})
var removed []string
for _, r := range b.cat.removed {
removed = append(removed, strings.TrimPrefix(r, name+"@"))
@@ -437,9 +560,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
defer store.Close()
u, _ := url.Parse(store.URL)
var open atomic.Bool
token, kept := "s3cret", true
f := &Front{Store: u, Window: &open, Published: func(name, tag string) (bool, error) { return name == "m/i" && tag == "0.1.3", nil },
Bootstrap: func() (string, bool) { return token, kept }}
f := &Front{Store: u, Window: &open, Published: func(name, tag string) (bool, error) { return name == "m/i" && tag == "0.1.3", nil }}
auth := ""
do := func(method, path string) int {
rec := httptest.NewRecorder()
@@ -468,24 +589,12 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Errorf("window open %v: %s %s = %d, want %d", c.open, c.method, c.path, got, c.want)
}
}
// The bootstrap's one-off credential writes with the window closed, never over a published tag, and not once
// its file is gone; a wrong token writes nothing.
// No credential opens the front (the lead's ENG-5 ruling): a write carrying a token, with the window closed, is
// refused as any other.
open.Store(false)
for _, c := range []struct {
auth string
kept bool
path string
want int
}{
{"s3cret", true, "/v2/gitea/program/manifests/0.1.0", http.StatusCreated},
{"s3cret", true, "/v2/m/i/manifests/0.1.3", http.StatusForbidden},
{"wrong", true, "/v2/gitea/program/manifests/0.1.0", http.StatusForbidden},
{"s3cret", false, "/v2/gitea/program/manifests/0.1.0", http.StatusForbidden},
} {
auth, kept = c.auth, c.kept
if got := do(http.MethodPut, c.path); got != c.want {
t.Errorf("bootstrap %q kept %v: PUT %s = %d, want %d", c.auth, c.kept, c.path, got, c.want)
}
auth = "s3cret"
if got := do(http.MethodPut, "/v2/gitea/program/manifests/0.1.0"); got != http.StatusForbidden {
t.Errorf("a write with a bearer token and the window closed = %d, want %d", got, http.StatusForbidden)
}
})
t.Run("mirror copies an outside image by its pinned digest, every platform, only the parts the store lacks", func(t *testing.T) {
@@ -593,6 +702,24 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Fatalf("two followers on one ask said %+v (ask id %q), asks %v: want one copy, said once under the ask's id", b.said[copies:], ask.ID, asked)
}
b.said = b.said[:copies]
// registry_mirrors (seams review note 1): a kept copy is read, asked again with its claim let go, and dropped
// by hand, which says refused under the ask's id with who and why.
_, _ = b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "nats", Digest: d})
asked[key] = MirrorRequest{Source: asked[key].Source, Digest: d, ID: asked[key].ID, Claim: "cut-follower"}
if now, err := b.v.MirrorsNow(); err != nil || now[key].Claim != "cut-follower" {
t.Fatalf("MirrorsNow = %v, %v; want the claimed ask", now, err)
}
if r, err := b.v.MirrorAgain(key); err != nil || r.Claim != "" || asked[key].Claim != "" {
t.Errorf("MirrorAgain = %+v, %v (kept %+v); want the claim let go", r, err, asked[key])
}
if _, err := b.v.MirrorDrop(key, "person.jochen", ""); err == nil {
t.Error("a drop with no why is taken")
}
if _, err := b.v.MirrorDrop(key, "person.jochen", "upstream gone"); err != nil || len(asked) != 0 ||
b.said[copies].event != EventRefused || b.said[copies].body.ID == "" || !strings.Contains(b.said[copies].body.Reason, "dropped by hand by person.jochen: upstream gone") {
t.Errorf("MirrorDrop = %v, said %+v, asks %v; want refused said under the ask's id and the ask gone", err, b.said[copies:], asked)
}
b.said = b.said[:copies]
b.cat.mirrorErr = errors.New("the upstream said 404")
_, _ = b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "nats", Digest: d})
if err := b.v.Mirrored(asked[key]); err != nil || b.said[1].event != EventRefused || !strings.Contains(b.said[1].body.Reason, "404") || len(asked) != 0 {
@@ -612,20 +739,26 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Errorf("direct writes held by %v, want g14", got)
}
t0 := time.Unix(0, 0)
w, closed, _ := Window{Name: OldNameWindow}.Step([]string{"g14"}, t0)
all := []string{"ace", "g14", "novox"}
w, closed, _ := Window{Name: OldNameWindow}.Step(all, []string{"g14"}, t0)
if closed != nil || !w.Open {
t.Fatalf("Step = %+v, %+v", w, closed)
}
if _, _, overdue := w.Step([]string{"g14"}, t0.Add(WindowFindingAfter)); !reflect.DeepEqual(overdue, []string{"g14"}) {
if _, _, overdue := w.Step(all, []string{"g14"}, t0.Add(WindowFindingAfter)); !reflect.DeepEqual(overdue, []string{"g14"}) {
t.Errorf("seven days: overdue %v", overdue)
}
// Security review F3: a read naming no node (an empty node_haves) judges nothing; the window stays open.
if same, shut, _ := w.Step(nil, nil, t0.Add(time.Minute)); shut != nil || !same.Open || !reflect.DeepEqual(same.Holding, []string{"g14"}) {
t.Errorf("a read naming no node: %+v, closed %+v; want the window open as it was", same, shut)
}
w, _ = w.Drop("g14", "person.jochen", "retired", t0)
w, closed, _ = w.Step(HoldingOldName(nodes, w.Dropped), t0.Add(time.Hour))
w, closed, _ = w.Step(all, HoldingOldName(nodes, w.Dropped), t0.Add(time.Hour))
if closed == nil || closed.Window != OldNameWindow || !reflect.DeepEqual(closed.LastHolding, []string{"g14"}) || len(closed.Dropped) != 1 {
t.Errorf("closing = %+v", closed)
}
if _, again, _ := w.Step([]string{"g14"}, t0.Add(2*time.Hour)); again != nil {
t.Error("a closed window closed again")
// Once closed, it stays closed: a node holding it later opens nothing.
if again, shut, _ := w.Step(all, []string{"g14"}, t0.Add(2*time.Hour)); shut != nil || again.Open {
t.Errorf("a closed window stepped on a holder: %+v, closed %+v; want it closed, said once", again, shut)
}
})
t.Run("the windows' driver: every node read on each wake, closed once, a paused build agent's same build counted", func(t *testing.T) {
@@ -647,6 +780,17 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
if !reflect.DeepEqual(st.w[OldNameWindow].Holding, []string{"g14"}) || !reflect.DeepEqual(st.w[DirectWritesWindow].Holding, []string{"g14"}) {
t.Errorf("windows = %+v, want g14 holding both (away: states nothing; runs the old build)", st.w)
}
// The window's record says why g14 holds it: its manifest-reads timed out, counted as stating nothing.
if why := st.w[OldNameWindow].Why["g14"]; !strings.Contains(why, "did not answer") || !strings.Contains(why, "timeout") {
t.Errorf("why g14 holds the old name = %q, want the timeout said", why)
}
// Security review F3: a wake whose node_haves names no node closes neither window.
haves := del.haves
del.haves = nil
if _, err := d.Wake("delivered"); err != nil || !st.w[OldNameWindow].Open || !st.w[DirectWritesWindow].Open || len(closed) != 0 {
t.Fatalf("a wake on an empty node_haves: %v, windows %+v, closed %v; want both open, none said", err, st.w, closed)
}
del.haves = haves
now = now.Add(WindowFindingAfter)
_, _ = d.Wake("delivered")
if !found[FindingWindowHeld+" "+OldNameWindow+".g14"] {
@@ -729,5 +873,28 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
if store < 0 || program < 0 || store > program {
t.Errorf("store at %d, program at %d: want the store listed first", store, program)
}
// Running review F2: incoming asks the bus 8 GiB, the most the objects template allows, equal to IncomingCap
// (said only where no binding is read); transfers keeps for good, the holder dropping an idle one by its key
// in idle, a state of keys with their own time to live.
var req struct {
Requires []struct {
Name string `json:"name"`
Options map[string]any `json:"options"`
} `json:"requires"`
}
_ = json.Unmarshal(raw, &req)
opts := map[string]map[string]any{}
for _, r := range req.Requires {
opts[r.Name] = r.Options
}
if mib, _ := opts[IncomingObjects]["max-mib"].(float64); uint64(mib)<<20 != IncomingCap {
t.Errorf("incoming asks %v MiB, IncomingCap is %d bytes: want them equal", opts[IncomingObjects]["max-mib"], uint64(IncomingCap))
}
if _, ok := opts[TransfersState]["ttl-seconds"]; ok {
t.Errorf("transfers has a lifetime %v: the bus would drop a transfer's key and leave its parts", opts[TransfersState])
}
if opts[IdleState]["key-ttl"] != true {
t.Errorf("idle's options %v: want key-ttl, each transfer's key ending on its own", opts[IdleState])
}
})
}
@@ -10,7 +10,10 @@ import (
)
// fakeRegistry is an OCI registry in memory, enough for a mirror's copy: blobs, uploads and manifests by
// reference. With token set it answers 401 with a Bearer challenge until a request carries it.
// reference. With token set it answers 401 with a Bearer challenge until a request carries it. A DELETE of a manifest
// answers as registry 2.8.3, the store's pinned image, does (distribution v2.8.3 registry/handlers/manifests.go,
// DeleteManifest): by digest it deletes the manifest and every tag naming it; by a tag it deletes nothing and answers
// 400 DIGEST_INVALID, the check of the empty digest it deletes instead failing.
type fakeRegistry struct {
mu sync.Mutex
blobs map[string][]byte // name@digest
@@ -69,6 +72,27 @@ func (f *fakeRegistry) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.Contains(p, "/manifests/"):
i := strings.Index(p, "/manifests/")
key := p[:i] + ":" + p[i+11:]
if r.Method == http.MethodDelete {
ref := p[i+11:]
if !strings.HasPrefix(ref, "sha256:") {
w.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(w, `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest did not match uploaded content"}]}`)
return
}
found := false
for k, b := range f.manifests {
if strings.HasPrefix(k, p[:i]+":") && digestOf(b) == ref {
delete(f.manifests, k)
found = true
}
}
if !found {
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusAccepted)
return
}
if r.Method == http.MethodPut {
b, _ := io.ReadAll(r.Body)
f.manifests[key] = b
@@ -98,7 +122,10 @@ type fakeDelivery struct {
func (f *fakeDelivery) Haves() ([]NodeHave, error) { return f.haves, nil }
func (f *fakeDelivery) Reads(node string) (bool, []string, error) {
s, ok := f.stated[node]
return ok, s, nil
if !ok {
return false, nil, errors.New("nats: timeout")
}
return true, s, nil
}
type memWindows struct {
+24 -1
View File
@@ -3,6 +3,7 @@ package holder
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -253,7 +254,29 @@ func (o *OCI) Digest(name, version string) (string, error) {
return resp.Header.Get("Docker-Content-Digest"), nil
}
// Remove deletes the manifest name holds at version.
// ErrNoTagAlone is a store that cannot delete a tag alone. registry 2.8.3, the store's pinned image, is one: its
// DeleteManifest (distribution v2.8.3 registry/handlers/manifests.go) ignores a tag reference and deletes the empty
// digest, whose check fails, answered 400 DIGEST_INVALID; registry 3 untags alone.
var ErrNoTagAlone = errors.New("the store cannot remove a tag alone")
// RemoveTag deletes the tag version of name alone (the OCI distribution protocol's DELETE of a manifest by its tag);
// the manifest stays under every other tag. A store that cannot delete a tag answers ErrNoTagAlone, any other failure
// an error of its own; either removes nothing.
func (o *OCI) RemoveTag(name, version string) error {
resp, b, err := o.do(http.MethodDelete, "/v2/"+name+"/manifests/"+version, "", nil)
if err != nil {
return err
}
if resp.StatusCode == http.StatusBadRequest && bytes.Contains(b, []byte(`"DIGEST_INVALID"`)) {
return fmt.Errorf("%w: DELETE of %s:%s by its tag answered 400 DIGEST_INVALID", ErrNoTagAlone, name, version)
}
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNotFound {
return fmt.Errorf("the store did not delete the tag %s:%s alone (%d): %s", name, version, resp.StatusCode, b)
}
return nil
}
// Remove deletes the manifest name holds at version, by its digest: every tag naming it goes with it.
func (o *OCI) Remove(name, version string) error {
d, err := o.Digest(name, version)
if err != nil || d == "" {
@@ -9,19 +9,26 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/parts"
)
// The bounds of incoming (design 51 *Publishing*, ADR 0611): a part is at most 1 MiB, the store 20 GiB, a transfer
// with no part for 24 hours is dropped, and past 80 % of the cap the holder writes a finding.
// The bounds of incoming (design 51 *Publishing*, ADR 0611): a part is at most 1 MiB, a transfer with no part for 24
// hours is dropped, and past 80 % of the cap the holder writes a finding. The cap is the one incoming's binding carries
// (the bus's own bound of the object store, read through the runtime); IncomingCap is the manifest's 8 GiB, the most
// the objects template allows, said only where no binding is read (a test), and the manifest's test holds the two equal.
const (
MaxPart = 1 << 20
IncomingCap = 20 << 30
IncomingCap = 8 << 30
TransferIdle = 24 * time.Hour
IncomingFullPct = 80
// IncomingObjects and TransfersState are the object store and the state distribution asks as bus state.
IncomingObjects = "incoming"
TransfersState = "transfers"
// IdleState is the state of one key per open transfer with its own time to live (a state declared key-ttl): the
// bus's marker of its end is the event that drops a transfer left with no part for TransferIdle (ADR 0611 §3), never
// a timer.
IdleState = "idle"
)
// The kinds a seat holds.
@@ -97,6 +104,8 @@ type Event struct {
// Reference is the seat's answer: `<registry>/<module>/<artifact>@sha256:<digest>` (ADR 0605 §9).
Reference string `json:"reference,omitempty"`
Reason string `json:"reason,omitempty"`
// Tags are, for kept, the other tags still naming its digest.
Tags []string `json:"tags,omitempty"`
// By is who asked a removal, hold or release: the caller the tool runner proved.
By string `json:"by,omitempty"`
// ID is the event's id on the bus when the outcome has one of its own (a mirror ask's): the same outcome said
@@ -109,6 +118,7 @@ const (
EventPublished = "published"
EventRefused = "refused"
EventRemoved = "removed"
EventKept = "kept"
EventHeld = "held"
EventReleased = "released"
)
@@ -156,8 +166,14 @@ type Holder struct {
Say Say
Finding Finding
Now func() time.Time
// Cap is incoming's cap; IncomingCap when zero.
Cap uint64
// Cap reads incoming's cap from its binding (parts.BusCap); IncomingCap when nil or when the binding names none.
Cap func() (uint64, error)
// Record records a hand act through the seat mesh-hand-acts (handacts.Recorder); a drop by hand is refused
// without it.
Record func(handacts.Request) error
// Idle writes a transfer's idle key with its own time to live, once (parts.BusArm over IdleState); the receiver
// arms it on each open that leaves a transfer open. Nil arms nothing.
Idle func(id string, ttl time.Duration) error
}
func (h *Holder) now() time.Time {
@@ -167,18 +183,14 @@ func (h *Holder) now() time.Time {
return time.Now().UTC()
}
func (h *Holder) cap() uint64 {
if h.Cap > 0 {
return h.Cap
}
return IncomingCap
}
// CapNow is incoming's cap as its binding carries it.
func (h *Holder) CapNow() uint64 { return h.receiver().CapNow() }
// receiver binds mesh-sdk's receiving side to distribution's incoming, its transfers state, the builder's check, its
// store and its seats' events.
func (h *Holder) receiver() *parts.Receiver[Opening] {
return &parts.Receiver[Opening]{Objects: h.Objects, Transfers: h.Transfers, Cap: h.cap(), MaxChunk: MaxPart,
Idle: TransferIdle, Full: IncomingFullPct, Now: h.now,
return &parts.Receiver[Opening]{Objects: h.Objects, Transfers: h.Transfers, Cap: IncomingCap, Bound: h.Cap,
MaxChunk: MaxPart, Idle: TransferIdle, Arm: h.Idle, Full: IncomingFullPct, Now: h.now,
// Only the missing layers are sent: a part the store already holds under the name counts as held.
Holds: func(t Transfer, digest string) bool {
ok, err := h.Store.Has(t.Shape.Name, digest)
@@ -283,10 +295,38 @@ func (h *Holder) eventOf(seat string, r PublishRequest, ref, reason string) Even
// Drop drops a transfer by hand (registry_incoming) or because it was idle for TransferIdle: its chunks and its key.
func (h *Holder) Drop(id string) (Transfer, error) { return h.receiver().Drop(id) }
// DropIdle drops every transfer with no part for TransferIdle, answering them; it runs on each publish's open and each
// registry_incoming call, never on a timer (the object store's own lifetime of 24 hours drops the bytes too).
// DropByHand is registry_incoming's drop: by a person, with why, recorded as a hand act through mesh-hand-acts before
// anything goes; an act not recorded leaves the transfer open.
func (h *Holder) DropByHand(id, by, why string) (Transfer, error) {
if strings.TrimSpace(why) == "" {
return Transfer{}, errors.New("a drop is a hand act: say why")
}
if h.Record == nil {
return Transfer{}, errors.New("no hand act can be recorded here, so nothing is dropped")
}
t, found, err := h.Transfers.Get(id)
if err != nil {
return Transfer{}, fmt.Errorf("transfer %s cannot be read, so nothing is dropped: %w", id, err)
}
if !found {
return Transfer{}, fmt.Errorf("transfer %s is not open", id)
}
what := fmt.Sprintf("dropped the open transfer %s of incoming (%s@%s on %s, opened by %s, %d parts lacked), by %s", id, t.Shape.Name, t.Shape.Version, t.Shape.Seat, t.Opener, len(t.Lacks()), by)
if err := h.Record(handacts.Request{What: what, Why: why, Cause: "registry_incoming"}); err != nil {
return Transfer{}, fmt.Errorf("the hand act cannot be recorded, so transfer %s stays open: %w", id, err)
}
return h.Drop(id)
}
// DropIdle drops every transfer with no part for TransferIdle, answering them: registry_incoming's look, by hand. The
// drop itself follows each transfer's idle key (Expired); the objects template keeps for good, so nothing else does.
func (h *Holder) DropIdle() ([]string, error) { return h.receiver().DropIdle() }
// Expired is told that a transfer's idle key ended (the bus's marker, parts.FollowIdle): the receiver drops a transfer
// with no part for TransferIdle, its chunks with it, arms one that took a part since again for the rest, and leaves one
// already gone. It answers whether it dropped the transfer.
func (h *Holder) Expired(id string) (bool, error) { return h.receiver().Expired(id) }
func checkPublish(seat string, r PublishRequest) error {
switch {
case !slices.Contains(Seats, seat):
+124 -14
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"slices"
"sort"
"strings"
"time"
@@ -18,8 +19,12 @@ type Catalog interface {
Versions(name string) ([]string, error)
// Digest is the digest name holds at version, empty when none.
Digest(name, version string) (string, error)
// Remove deletes name at version (its manifest); the store's collector frees its blobs.
// Remove deletes name at version by its manifest's digest, every tag naming that digest with it; the store's
// collector frees its blobs.
Remove(name, version string) error
// RemoveTag deletes the tag version of name alone, the manifest staying under every other tag naming it; a store
// that cannot delete a tag alone answers ErrNoTagAlone and removes nothing.
RemoveTag(name, version string) error
// RemoveDigest deletes an untagged manifest of name by its digest (retention).
RemoveDigest(name, digest string) error
// Platforms are the manifests an index of name names; none for a manifest that is not an index or is not held.
@@ -42,6 +47,10 @@ type Mirrors interface {
// writer wrote the key first.
Claim(key string, r MirrorRequest, revision uint64) (bool, error)
Delete(key string) error
// List is every ask kept, by its key: what registry_mirrors reads.
List() (map[string]MirrorRequest, error)
// Put writes r under key whatever it held: an ask asked again by hand, its claim let go.
Put(key string, r MirrorRequest) error
}
// HoldState is distribution's own state holds (bus state, template state; ADR 0617 §2).
@@ -114,48 +123,100 @@ type RemoveRequest struct {
// The seat definitions name it (the removal reasons' enum), so the sweep and the holder share one value.
const ReasonRetention = reg.RemoveInputReasonRetention
// Removal is what remove answers: Removed, or Kept with its reason and the other tags naming its digest.
type Removal struct {
Removed bool
Kept bool
Reason string
Tags []string
}
// ReasonSharedDigest is why a removal was kept: another tag names its digest, and the store cannot remove a tag alone.
const ReasonSharedDigest = reg.RemoveAnswerReasonSharedDigest
// Remove is `remove`: the builder removes a tagged version; the artifact sweep's holder an untagged bundle, archive or
// copied base with the reason retention, which a hold keeps. Every removal says `removed` with its reason.
func (v *Verbs) Remove(seat, caller string, r RemoveRequest) error {
func (v *Verbs) Remove(seat, caller string, r RemoveRequest) (Removal, error) {
if err := v.Callers.Allowed(VerbRemove, caller); err != nil {
return err
return Removal{}, err
}
if strings.TrimSpace(r.Name) == "" || strings.TrimSpace(r.Reason) == "" {
return errors.New("remove: a name and a reason are required")
return Removal{}, errors.New("remove: a name and a reason are required")
}
ev := Event{Name: r.Name, Version: r.Version, Kind: r.Kind, Digest: r.Digest, Holder: Self, Reason: r.Reason, By: caller}
switch {
case r.Reason == ReasonRetention:
if CallerModule(caller) != SweepHolder {
return fmt.Errorf("remove: refused for %s: a removal for retention is the artifact sweep's alone", caller)
return Removal{}, fmt.Errorf("remove: refused for %s: a removal for retention is the artifact sweep's alone", caller)
}
if r.Digest == "" {
return errors.New("remove: a removal for retention names the digest")
return Removal{}, errors.New("remove: a removal for retention names the digest")
}
if _, held, err := v.Holds.Get(Hold{Seat: seat, Name: r.Name, Digest: r.Digest}.Key()); err != nil {
return fmt.Errorf("remove: the holds cannot be read, so nothing is removed: %w", err)
return Removal{}, fmt.Errorf("remove: the holds cannot be read, so nothing is removed: %w", err)
} else if held {
return fmt.Errorf("remove: refused: %s@%s is held; release it first", r.Name, r.Digest)
return Removal{}, fmt.Errorf("remove: refused: %s@%s is held; release it first", r.Name, r.Digest)
}
if err := v.removeForRetention(seat, r.Name, r.Digest); err != nil {
return err
return Removal{}, err
}
default:
if r.Version == "" || !strings.Contains(r.Version, "-") {
return fmt.Errorf("remove: refused: %s@%s has no tag; only a tagged version is removed, but for retention", r.Name, r.Version)
return Removal{}, fmt.Errorf("remove: refused: %s@%s has no tag; only a tagged version is removed, but for retention", r.Name, r.Version)
}
d, err := v.Catalog.Digest(r.Name, r.Version)
if err != nil {
return fmt.Errorf("remove: the store cannot read %s@%s: %w", r.Name, r.Version, err)
return Removal{}, fmt.Errorf("remove: the store cannot read %s@%s: %w", r.Name, r.Version, err)
}
if d != "" {
if err := v.Catalog.Remove(r.Name, r.Version); err != nil {
return fmt.Errorf("remove: the store did not remove %s@%s: %w", r.Name, r.Version, err)
// Reproducible builds share a digest between a branch's tagged version and the trunk's numbered one: a
// manifest deleted by its digest would take the trunk's version with it (security review F1). When another
// tag names the digest, the tag alone goes. A store that cannot remove a tag alone (registry 2.8.3) keeps the
// version whole, its tag and its bytes, which the other tags name anyway: remove answers and says kept with
// the reason shared-digest and those tags, no refusal, so the builder raises no retry. Any other failure is
// refused.
others, err := v.otherTags(r.Name, r.Version, d)
if err != nil {
return Removal{}, fmt.Errorf("remove: the store cannot say which other tags of %s name %s, so nothing is removed: %w", r.Name, d, err)
}
if len(others) > 0 {
if err := v.Catalog.RemoveTag(r.Name, r.Version); errors.Is(err, ErrNoTagAlone) {
sort.Strings(others)
ev.Digest, ev.Reason, ev.Tags = d, ReasonSharedDigest, others
kept := Removal{Kept: true, Reason: ReasonSharedDigest, Tags: others}
return kept, v.Say(seat, EventKept, ev)
} else if err != nil {
return Removal{}, fmt.Errorf("remove: refused: %s@%s shares its digest %s with %v, and the store did not remove the tag alone, so nothing is removed: %w", r.Name, r.Version, d, others, err)
}
} else if err := v.Catalog.Remove(r.Name, r.Version); err != nil {
return Removal{}, fmt.Errorf("remove: the store did not remove %s@%s: %w", r.Name, r.Version, err)
}
}
ev.Digest = d // a version already gone is said removed all the same, so the builder's retry ends
}
return v.Say(seat, EventRemoved, ev)
return Removal{Removed: true}, v.Say(seat, EventRemoved, ev)
}
// otherTags are the tags of name but version that name digest.
func (v *Verbs) otherTags(name, version, digest string) ([]string, error) {
tags, err := v.Catalog.Versions(name)
if err != nil {
return nil, err
}
var out []string
for _, t := range tags {
if t == version {
continue
}
d, err := v.Catalog.Digest(name, t)
if err != nil {
return nil, err
}
if d == digest {
out = append(out, t)
}
}
return out, nil
}
// HoldRequest is `hold` and `release`.
@@ -316,6 +377,55 @@ func (v *Verbs) Take(key string, r MirrorRequest, revision uint64, current bool,
return v.Mirrored(r)
}
// MirrorsNow lists every copy `mirror` took on and has not ended, by its key, for registry_mirrors: what waits (no
// claim) and what a follower took on (its claim).
func (v *Verbs) MirrorsNow() (map[string]MirrorRequest, error) { return v.Mirrors.List() }
// MirrorAgain asks a kept copy again by hand (registry_mirrors, a hand act): its claim is let go, so the state's watch
// hands it to a follower, which takes it on as a new ask. A copy whose follower was cut, or that a follower holds and
// does not end, goes on this way without a restart.
func (v *Verbs) MirrorAgain(key string) (MirrorRequest, error) {
all, err := v.Mirrors.List()
if err != nil {
return MirrorRequest{}, fmt.Errorf("mirror: the asks cannot be read: %w", err)
}
r, ok := all[key]
if !ok {
return MirrorRequest{}, fmt.Errorf("mirror: no copy is kept under %s", key)
}
r.Claim = ""
if err := v.Mirrors.Put(key, r); err != nil {
return MirrorRequest{}, fmt.Errorf("mirror: %s is not asked again: %w", key, err)
}
return r, nil
}
// MirrorDrop drops a kept copy by hand (registry_mirrors, a hand act with who and why): it says refused on the image
// seat's events with that reason, under the ask's id, so a waiter on the copy ends, and the ask leaves the state.
func (v *Verbs) MirrorDrop(key, by, why string) (MirrorRequest, error) {
if strings.TrimSpace(why) == "" {
return MirrorRequest{}, errors.New("mirror: a drop is a hand act: say why")
}
all, err := v.Mirrors.List()
if err != nil {
return MirrorRequest{}, fmt.Errorf("mirror: the asks cannot be read: %w", err)
}
r, ok := all[key]
if !ok {
return MirrorRequest{}, fmt.Errorf("mirror: no copy is kept under %s", key)
}
src, err := ParseSource(r.Source)
if err != nil {
return MirrorRequest{}, err
}
e := Event{Name: src.Name(), Kind: KindUpstream, Digest: r.Digest, Holder: Self, By: by, ID: r.ID,
Reason: fmt.Sprintf("%s@%s was not copied: dropped by hand by %s: %s", r.Source, r.Digest, by, why)}
if err := v.Say(ImageSeat, EventRefused, e); err != nil {
return MirrorRequest{}, fmt.Errorf("mirror: the drop of %s cannot be said, so the ask stays: %w", key, err)
}
return r, v.Mirrors.Delete(key)
}
// HeldNow lists the holds, for registry_holds.
func (v *Verbs) HeldNow() ([]Hold, error) {
all, err := v.Holds.List()
+53 -37
View File
@@ -2,6 +2,8 @@ package holder
import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"slices"
"sort"
"time"
@@ -16,10 +18,6 @@ import (
const (
OldNameWindow = "old-name"
DirectWritesWindow = "direct-writes"
// BootstrapWindow is the bootstrap's one-off credential (BootstrapFile): open while the file is there. The tools
// bundle closes it at its first answered publish; the program, which runs as distribution's account and alone may
// remove the file in distribution's state directory, removes it on that close.
BootstrapWindow = "bootstrap-push"
// WindowFindingAfter is how long a node holds a window open before it is distribution's finding naming the node.
WindowFindingAfter = 7 * 24 * time.Hour
// The events a window's close says on distribution's own family distribution.events (7 days, 16 MiB, drop
@@ -31,16 +29,16 @@ const (
)
// Window is one window's record, kept in distribution's state windows: what registry_old_name and
// registry_direct_writes answer.
// registry_direct_writes answer. Its open, holding, since, closed and last holding are mesh-sdk's delivery.WindowHeld,
// the part every window keeper shares and steps by one rule (delivery.StepWindow).
type Window struct {
Name string `json:"name"`
Open bool `json:"open"`
Holding []string `json:"holding,omitempty"`
Since map[string]time.Time `json:"since,omitempty"`
Dropped []DroppedNode `json:"dropped,omitempty"`
Closed time.Time `json:"closed,omitzero"`
// LastHolding are the nodes that held it last, said when it closes.
LastHolding []string `json:"last_holding,omitempty"`
Name string `json:"name"`
delivery.WindowHeld
Dropped []DroppedNode `json:"dropped,omitempty"`
// Why says, of each node holding the window, why it holds it: what its read answered, a node-engine whose
// manifest-reads did not answer (an away node, an older node-engine, a timeout) included, so a timeout taken as
// stating nothing is told apart from a node-engine that states the old names.
Why map[string]string `json:"why,omitempty"`
}
// DroppedNode is one retired node dropped from a window by hand: a hand act.
@@ -61,16 +59,42 @@ type Closed struct {
}
// NodeRead is what distribution read of one node: the reader capabilities its node-engine states through
// `manifest-reads` (Serves false while it does not serve the verb: it states nothing), whether its declaration names an
// artifact, and its build modules' haves: whether each runs a build that publishes through the seats.
// `manifest-reads` (Serves false while it does not serve the verb: it states nothing; NotServed then says what the
// call answered), whether its declaration names an artifact, and its build modules' haves: whether each runs a build
// that publishes through the seats.
type NodeRead struct {
Node string `json:"node"`
Serves bool `json:"serves"`
NotServed string `json:"not_served,omitempty"`
Stated []string `json:"stated,omitempty"`
NamesArtifact bool `json:"names_artifact"`
BuildModules map[string]bool `json:"build_modules,omitempty"`
}
// WhyOldName says why a node holds the old name open.
func WhyOldName(n NodeRead) string {
if !n.Serves {
if n.NotServed != "" {
return "its node-engine's manifest-reads did not answer, so it counts as stating nothing: " + n.NotServed
}
return "its node-engine does not serve manifest-reads, so it states nothing"
}
return "its node-engine does not state " + NewNamesCapability
}
// WhyDirectWrites says why a node holds the direct writes open: its build modules running a build not heard
// publishing through the seats.
func WhyDirectWrites(n NodeRead) string {
var old []string
for m, publishes := range n.BuildModules {
if !publishes {
old = append(old, m)
}
}
sort.Strings(old)
return fmt.Sprintf("its %v run a build not heard publishing through the seats", old)
}
// HoldingOldName are the nodes holding the old name open: a node whose declaration names an artifact and whose
// node-engine does not state the new names (one not serving manifest-reads states nothing).
func HoldingOldName(nodes []NodeRead, dropped []DroppedNode) []string {
@@ -110,33 +134,25 @@ func isDropped(dropped []DroppedNode, node string) bool {
return slices.ContainsFunc(dropped, func(d DroppedNode) bool { return d.Node == node })
}
// Step moves the window to the nodes holding it at now. It answers the next record, the closing event when it closed in
// this step (said once), and the nodes holding it past WindowFindingAfter (the finding; cleared when it closes). A
// window once closed stays closed: a node that holds it later is no reason to serve the old name again.
func (w Window) Step(holding []string, now time.Time) (next Window, closed *Closed, overdue []string) {
next = Window{Name: w.Name, Dropped: w.Dropped, Closed: w.Closed, LastHolding: w.LastHolding}
// Step moves the window by mesh-sdk's one rule (delivery.StepWindow): named is every node the read named, dropped ones
// included, holding those holding the window at now. A read naming no node judges nothing. It answers the next record,
// the closing event when it closed in this step (said once), and the nodes holding it past WindowFindingAfter (the
// finding; cleared when it closes). A window once closed stays closed: a node that holds it later is no reason to serve
// the old name again or to take direct writes again.
func (w Window) Step(named, holding []string, now time.Time) (next Window, closed *Closed, overdue []string) {
next = w
if !w.Open && !w.Closed.IsZero() {
return next, nil, nil
}
if len(holding) == 0 {
next.Closed, next.LastHolding = now, w.Holding
if next.LastHolding == nil {
next.LastHolding = []string{}
}
return next, &Closed{Window: w.Name, At: now, LastHolding: next.LastHolding, Dropped: w.Dropped}, nil
held, shut, overdue := delivery.StepWindow(w.WindowHeld, named, holding, now, WindowFindingAfter)
next.WindowHeld = held
if !held.Open {
next.Why = nil
}
next.Open, next.Holding, next.Since = true, holding, map[string]time.Time{}
for _, n := range holding {
since, had := w.Since[n]
if !had {
since = now
}
next.Since[n] = since
if now.Sub(since) >= WindowFindingAfter {
overdue = append(overdue, n)
}
if !shut {
return next, nil, overdue
}
return next, nil, overdue
return next, &Closed{Window: w.Name, At: held.Closed, LastHolding: held.LastHolding, Dropped: w.Dropped}, nil
}
// Drop drops a retired node from the window by hand, a hand act with who and why.
+9 -2
View File
@@ -48,13 +48,15 @@
"registry_health",
"registry_holds",
"registry_incoming",
"registry_mirrors",
"registry_old_name"
],
"invokes": [
"distribution.registry_callers",
"mesh-delivery-builder.builder_versions",
"mesh-delivery-view.node_haves",
"seat:node-engine.manifest-reads"
"seat:node-engine.manifest-reads",
"seat:mesh-hand-acts.hand-act"
],
"consumes": [
"mesh-delivery-view.delivered",
@@ -83,9 +85,14 @@
{
"name": "transfers",
"storage": "key-value",
"template": "state"
},
{
"name": "idle",
"storage": "key-value",
"template": "state",
"options": {
"ttl-seconds": 86400
"key-ttl": true
}
},
{
+2 -2
View File
@@ -2,10 +2,10 @@ module dmenu
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module dnsmasq
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module docker-compose
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module docker
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module dunst
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module fail2ban
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module feh
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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
+6 -4
View File
@@ -1,7 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/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=
+2 -2
View File
@@ -2,10 +2,10 @@ module flatpak
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // 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

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