Commit Graph
28 Commits
Author SHA1 Message Date
jochen 1dca181e03 Put every repository path through the segment helper, gitea_open_pulls's included
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
After merging #159 (issue 347): every /repos/<owner>/<name> the client builds
now takes its owner and name through one helper, whether they came from a
caller or from the forge's answer; the open-pulls fixture's shas are commits.
2026-10-09 13:41:35 +02:00
jochen 46fe6fff64 Merge remote-tracking branch 'origin/main' into fix/the-forge-tools-never-weaken-protection
# Conflicts:
#	modules/gitea/client.ts
2026-10-09 13:41:02 +02:00
jochen 5eab586116 Put no caller's text in a forge path unjudged: every segment checked, every sent path checked again
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
The confirmation review of 2026-10-09 found the dedicated tools building paths
from caller text: a branch named .. became DELETE /repos/o/r/ (the repository),
a file path ../../../../version reached /api/v1/version, and a sha with
?sudo= set a status as any user. Each caller-supplied segment now goes through
one helper refusing empty, . and .. and / ? # %; a sha must be a commit; and
the client judges every path once more as it sends it: no dot or empty
segment, encoded or not, and only the allow-listed query keys.
2026-10-09 13:35:15 +02:00
jochen 32d55beb47 Say a pull request nobody checks as a delivery held, owned by mesh-delivery (hq issue 347)
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
The confirmation review asked for the delivery domain's own vocabulary
instead of a provider standing. A gitea health check would need a `tools`
list (turning gitea's tools into seat verbs), a check of another kind
beside it, and would hold the forge's own deliveries at the first-node
gate; so the delivery's owner says it, with the word it already has.

- gitea_open_pulls (gitea): every open pull request from the forge itself,
  with what its base requires and the statuses its head has.
- mesh-delivery asks it every five minutes and keeps its own first-seen
  time per head (the forge's update time moves on every comment). A head
  on a base requiring mesh/merge-gate OR mesh/repo-check (hq requires only
  the second) with neither for ten minutes is a `stalled` line in state
  `unannounced`, which the controller raises as delivery.<id>.stalled (D14);
  H2 has nothing to close. Once announced it is a delivery of its own.
- The gitea-side watcher and its provider standing are gone.
2026-10-09 13:14:36 +02:00
jochen ea772a9cce Let gitea_api send only the query keys it needs, never sudo, and never approve a pull request
The confirmation review of 2026-10-09 measured GET /user?sudo=… answer the
operator's own account: the query went to the forge unjudged, and the tools'
token is the site administrator's, so any caller could act as any user.
gitea_api now sends only allow-listed query keys and refuses sudo in any case,
and a review that approves or requests changes is the operator's.
2026-10-09 12:57:16 +02:00
jochen 67bce7e189 Announce a pull request opened after its push, and say one nobody checks (hq issue 347)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
The announcer read only the repositories whose update time moved since its
last pass, less a minute. Opening, reopening or retargeting a pull request
does not move a repository's update time, so a pull request opened more
than a minute after its push was never announced, never checked, and
nothing said so (mesh-sdk #14, 2026-10-09).

- Each pass also looks at every repository holding a pull request updated
  since the floor, by the forge's own search; every ten minutes, or when the
  search fails, at every repository.
- Every five minutes the forge is asked for every open pull request; one on
  a base requiring mesh/merge-gate whose head has had none for ten minutes
  is said as the forge failing that pull request's merge check (a provider
  standing, ADR 0224), which the controller keeps as a condition, and is
  recovered when a check arrives or it closes.
- The client refuses a pull request or issue number that is not one before
  asking the forge (a misnamed tool argument sent pulls/NaN).
2026-10-09 12:55:09 +02:00
jochen ff648ad32b Send the path gitea_api judged, keep a pull request's base, merge only into a guarded branch, and delete only the tools' own idle repositories
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
Follow-ups of the review of 2026-10-09: the escape hatch sent the raw path it
had judged in normalised form; a pull request's base could be moved through
it; the merge tool would merge into a branch no rule guards; a rule named for
a branch could undercut the glob rule guarding it; and the delete tool took
any unguarded repository. It now takes only a repository of the tools' own
user with no protection and no commit in 30 days, named in full.
2026-10-09 12:26:37 +02:00
jochen 080c113d69 Let the forge's tools never weaken protection or merge past the checks
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
gitea_api took any endpoint with a token minted for the forge's
administrator with write:admin: an agent could drop a branch's protection
or its required checks, set the merge check's status, write to main or
merge its own pull request (novox/hq ADR 0266 §7, the review of
2026-10-09). gitea_api now reads anything but the administration and
writes only what an author writes; protection is only strengthened; a
merge needs every required status a success and merges only that head; a
guarded repository is not deleted; and the token is minted without
write:admin, a kept one re-minted at the next start.
2026-10-09 11:47:36 +02:00
jochen 0bd50fce0a The forge module's tool list test names the statuses and contains tools
mesh/merge-gate pass: builds mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
It had not been run before a merge: its expected list lacked gitea_commit_statuses since that tool came,
and now gitea_contains. The catalogue's TypeScript check, merged in from #151, runs it.
2026-10-09 03:23:19 +02:00
jochen 6327f297ea Cover a held merge only when the forge says a later delivered merge contains it (review of #150)
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module
gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery
asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that
cannot say leaves the merge held, not covered. Merge order stays the cheap first filter.

A later delivery now carries a module only when its own check passed (so a merge held without a passing
check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its
steps say the module reached every machine running it, not one. The words say what is known: a later
merge that contains it was delivered.
2026-10-09 02:57:18 +02:00
jochen 74f912e1fb Put the merge check back to pending before a recheck asks it (hq issue 305)
mesh/repo-check pass: its merge-check.sh passed
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/delivery delivered
A recheck asks of the head the forge already judged, and the forge keeps
the newest status of each context on it. Nobody reset mesh/merge-gate or
mesh/repo-check, so the old green stood and branch protection would merge
on it while the fresh check ran; rechecked heads did turn red.

mesh-delivery's recheck now asks the forge's holder to set the gate, and
the repository check when the head holds one, to pending ("checking
again: <why>") before it asks the controller; a forge that cannot be told
refuses the recheck whole. The gitea holder takes pending, and only
pending, on the merge check's statuses by hand: no verdict, so issue 293
still holds.
2026-10-08 10:11:26 +02:00
mesh-admin 9d4bbbd2bd Merge pull request 'Answer what the mesh's checks said of a pull request: mesh-delivery's checks verb (hq ADR 0239)' (#109) from feat/delivery-checks-verb into main 2026-10-07 19:04:10 +00:00
jochen 73bb597c16 Hold what the tools say to the glossary's retired words (hq ADR 0244)
mesh/merge-gate pass: builds docker, gitea, lab, nftables, slack, systemd → ace, g14, novox, shanks; no bus step; 4 wait(s) for a person; every machine com…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
An agent meets the mesh's words most often in tool descriptions, and
nothing compared them with the glossary: several still said "the host"
for the node-engine and the forge's pull request comment was headed
"Change plan", a word retired twice over. retired-words is the copy of
the words the glossary retires for the tools, and checks/words fails the
repository check when any string a module's code can show, or any
manifest description, uses one. Those found are reworded here.
2026-10-07 20:02:09 +02:00
jochen 13894744a7 Answer what the mesh's checks said of a pull request, as mesh-delivery's checks verb (hq ADR 0239)
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
mesh/delivery-group group feat/delivery-checks-verb delivered: every member is delivered
Reading a pull request's mesh/merge-gate and mesh/repo-check meant scraping the controller's journal:
the forge clips each status to 140 characters and nothing returned the verdict whole. mesh-delivery
already keeps the verdict; it now keeps the machine that ran it, the layers' modules and the report,
and its checks verb joins that to the forge's statuses, read through a new gitea_commit_statuses tool
that also says whether the base's protection lets the commit merge.

after: novox/mesh-controller
2026-10-07 19:36:41 +02:00
jochen 33857626be Never set warning on the merge check's statuses: the forge blocks a required one
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Branch protection requires mesh/merge-gate (and mesh/repo-check on the core
repositories) with no admin override, and the forge combines warning as a
failure. A note is now a success that says it; a repository without a
merge-check.sh is a success where repo-check is not required and a failure
for a person where it is; the status tool refuses the merge check's contexts.
2026-10-07 13:55:34 +02:00
jochen b6f0bc309b Add mesh-delivery, the owner of deliveries and delivery groups (hq ADR 0239)
mesh/merge-gate fail: a manifest the change touches fails the module check: modules/mesh-delivery/module.json: this manifest cannot be used:
mesh/delivery delivered
mesh/delivery-group group feat/mesh-delivery delivered: every member is delivered
One module answers 'did my change go out' for a commit and orders a
cross-repository change: one compiled state table, its state on the bus,
every transition said, noted on the commit and shown on the pull request.
The forge's holder gains the note, view and status tools it asks with, and
says closed pull requests and a merge's head and statuses.
2026-10-06 23:59:54 +02:00
jochen cc1305a354 gitea: post a pull request's change plan with its verdict (hq ADR 0238)
mesh/merge-gate pass: the merge check passed
mesh/delivery superseded: a newer head of the same pull request
The gate's status says what the change does and how it was judged; a change that builds
something gets its plan as a comment — the tiers, what each machine receives, and what
is not an ordinary send.
2026-10-06 22:50:32 +02:00
jochen dba71a97a8 gitea: tell the controller which files a pull request deletes; say the dependents a merge would build (hq ADR 0238)
The controller asks its planner what a pull request reaches, as if merged: a module whose
manifest the change deletes is one the merge removes, and the plan's dependents are said
on the gate's status beside the modules it moves.
2026-10-06 22:34:53 +02:00
jochen bd7b757dd9 gitea: give the controller what it maps a pull request onto the graph with; set both check statuses; protect a branch by tool (hq ADR 0237)
The controller now decides what a pull request's check runs from the mesh's module
graph, so the announcement carries the changed directories that hold a module at the
head and whether the head has a merge-check.sh. A verdict sets mesh/merge-gate (the
gate, with the modules it judged) and mesh/repo-check (the repository's own tests).
gitea_branch_protection_get/set let the operator's agent make those statuses required.

The catalogue's merge-check.sh leaves the gate to the build seat and keeps its own layer:
every manifest through module check, and the touched Go modules' tests.
2026-10-06 21:56:04 +02:00
jochen 9951718623 Check every pull request before it merges, and show the verdict on it (hq ADR 0237, to-be 45 §9)
The forge's announcer announces each new head of an open pull request as pull.updated, once,
and marks the head pending; the controller asks the build seat to check it against every
machine of the mesh's facts, and says the verdict as checked, which the forge's holder sets as
the head commit's status mesh/merge-gate - an error never as a success - with the check's own
account as a comment when it is not a pass. merge-check.sh is the catalogue's check: every
manifest through the running controller's module check and merge gate, and the Go tests of each
module the change touches, a module whose dependencies cannot be fetched said as not tested.
2026-10-06 21:16:32 +02:00
jochen 1178db279e gitea: say which changed directories hold a module at the merge commit (hq issue 278)
The controller read a changed file as shared code unless its directory was a module it holds or
the merge also changed that directory's manifest. A merge touching modules/showcase/index.ts -
the catalogue's reference module, held by no machine - therefore rebuilt all 103 modules built
from this repository on 2026-10-06, 88 of them byte-identical, with the build agent first only
because everything else is built by it.

Whether a directory is a module is a fact of the repository at the commit, so the announcer now
looks it up: every directory above a changed file (never the root) is asked for its module.json
at the merge commit, and the ones that have one go out as module_dirs, with module_dirs_said.
Not said when the file list is cut, past 300 directories, or when the forge cannot be asked;
the controller then keeps its old rule, which rebuilds too much rather than too little.

modules/showcase stays: TestTheShowcaseModuleIsAValidManifest in mesh-controller parses it and
hq to-be 18 and 20 name it as the reference module.
2026-10-06 19:55:17 +02:00
jochen 1fd2914ae1 Say which modules wait for a person's push, and announce the files a merge deleted (hq ADR 0236)
With a gate on the first machine and a rollback after it, a module's build rolls out by
default. The ones kept back say why: the network path a rollback could not cross, the
providers every consumer on a machine drops with, and the stores holding the photos.
A merge's deleted files are announced, so a module whose manifest went is forgotten
rather than asked to build (the public-acme plan failure).
2026-10-06 18:39:13 +02:00
jochen 52b81d524c gitea: a merge poll looks only at repositories that moved, one pass at a time
With the poll the only announcer of a merge, its cost showed: every 30 s it
asked every repository for its pull requests, a pass outlasted the tick, and
passes piled up beside each other — a merge was announced four and a half
minutes late, and two passes at once could each announce it. A pass now asks
only repositories updated since a minute before the last look, and the next
pass starts when this one ends. hq issue 250.
2026-10-05 18:12:11 +02:00
jochen 45507c3d5c gitea: test that a pull request's files are read past the forge's page cap 2026-10-05 17:47:36 +02:00
jschoubben 171f8a03f6 The forge's tools close and read pull requests, read files and branches, and delete a branch
Ten tools the console lacked for the actions a review and a merge leave behind: close or reopen a
pull request whose work landed elsewhere, change its title or body, read its files, its diff and its
comments, reopen an issue, read one file at a ref, list branches, delete the branch a closed pull
request leaves. Each is the client's own call; `gitea_api` stays the escape hatch for the rest.
Tested against the fake forge through the compiled tools, the way the console calls them (13/13).
2026-10-01 11:25:34 +02:00
jschoubben d58ed21367 gitea: the tools' token carries write:admin, and a kept token is re-minted when it lacks a scope
The forge's own users are the mesh's to settle — making the builder's login
a site admin so private repos build (hq 229) — and the tools' token had no
write:admin. A token kept from before a scope was added lacks it, so the
client now treats the forge's 403 "required scope" like a 401: the source
re-mints by name with the whole list and retries once. The fake forge in the
tests learns /repos/search, which the client has used since 2026-09-28 and
which had left 9 of the 11 token tests failing on main.
2026-09-30 21:05:52 +02:00
jschoubben aaf341a2d8 gitea: the token needs read:user, not just write:repository and write:issue
Deployed #49 and the watcher immediately broke: GET /user/repos answered 403,
'required=[read:user]' — confirmed live against the running forge (1.27.3).
That route sits under gitea's user scope category despite listing
repositories, not repository as assumed.

Also gives the fake forge real scope enforcement on /user/repos, which is
why the original PR's test suite didn't catch this: it only checked the
token's value was valid, never that it carried the required scope.
2026-09-24 11:43:46 +02:00
jschoubben ed094031fd The forge mints its own API token with the admin account the vault delivers
The runtime beside the forge served 0 tools: GiteaClient.fromEnv required a token
(settings or MESH_GITEA_TOKEN), nobody had one to give — the mesh raised the forge —
and putting one in settings would store a secret in plaintext in the inventory. So
the fifteen tools registered nothing and the watcher logged "not watching".

What the mesh does deliver is the admin account: a login the manifest names and a
password the vault minted and the host unsealed into a file (ADR 0086). That is
enough to mint a token, so the module does (hq issue 100, the forge's tools):
POST /users/{admin}/tokens over basic auth, scoped to write:repository and
write:issue — the least the tools and the repo watcher need — kept at 0600 in the
module's own state (/var/lib/mesh/gitea/state, a new directory resource the runtime
mounts writable), read back on the next start, and minted afresh when the forge
answers 401 to it or the kept file is gone. A forge whose data came from the
predecessor has no mesh-admin: that is reported in plain words on every poll until
it clears, once per reason, not crash-looped. A configured token still wins and is
never minted over.

The mint happens on the first call, not at registration: a contributor is
synchronous, and a forge not yet answering must not keep the runtime from serving.
One source per kept file in a process, or the watcher and the tools would each
renew on a 401 and drop the other's token by name.
2026-09-23 23:48:36 +02:00