gitea: the forge's tools never weaken protection or merge past the checks (hq ADR 0266 §7, route B) #153

Merged
mesh-admin merged 7 commits from fix/the-forge-tools-never-weaken-protection into main 2026-10-09 11:49:43 +00:00
Contributor

Based on main; independent of #140/#141 (neither touches modules/gitea).

The route

gitea_api (tools/index.ts) sent any method to any path with the tools' token, minted for the forge's site administrator mesh-admin with write:repository and write:admin (token.ts). An agent could drop a branch's protection or its required checks, set mesh/merge-gate to success through /statuses, write to main through /contents, make itself a collaborator or site admin, delete and recreate a repository the mesh builds from, or merge its own pull request past the checks. gitea_branch_protection_set could also weaken a rule directly.

The fix (guard.ts, pure, tested without a forge)

  • gitea_api reads anything but /admin and /users/*/tokens; writes only what an author writes: issues, comments, labels, pull requests (opened, edited, reviewed — never merged), branches, and a file on a branch that is neither the default branch nor one a protection rule (or glob) names. Everything else is refused before it is sent. Paths with dot segments or encoded slashes/dots are refused.
  • gitea_merge_pull_request reads the base's protection and the head's statuses and merges only when every required status is a success (mergeableUnder, the same judge gitea_commit_statuses uses); sends force_merge: false and head_commit_id, so the forge refuses if the head moved after the check; refuses manually-merged. The operator-approved merge stays: after the operator's yes, the agent's call merges a pull request whose checks passed.
  • gitea_branch_protection_set only strengthens: dropping a required status, allowing direct push, or allowing an admin override is refused.
  • gitea_delete_repo refuses a repository any protection rule guards.
  • Every tool's owner/repo must be a name, never another path.
  • The token is minted without write:admin. Nothing in the module's code used it (the provisioner settles users with the admin login over basic auth, GiteaAdmin); it was there for hand calls through gitea_api (hq 229). Its scopes are now kept beside it, and a token kept with another list is minted again at the next start — the forge drops the old one by name, so the scope does not outlive this.

What breaks

  • Settling the forge's users or teams by hand through gitea_api (hq 229's builder-as-site-admin was done that way) — now the operator's, at the forge as themselves, or a dedicated tool later.
  • Writing a file straight to main through gitea_api — use a branch and a pull request.
  • Deleting a protected repository through a tool.

Not done: a non-admin account for the tools

The token still belongs to mesh-admin, a site administrator, so scopes alone do not stop protection edits (they sit under repository) — the tools' guards do. Moving the tools to a non-admin user is feasible but larger: that user needs write access to every repository it merges and sets statuses on (a team in novox), the note append already runs in the forge's container, and creating a repository in an organisation needs membership. Proposed as a follow-up.

Tests (test/guard.test.ts; each fails before, passes after)

Against a fake forge: 15 refused gitea_api calls (admin, protection delete/patch, merge-gate status, merge with force_merge, collaborator, hook, key, repo settings, repo delete, transfer, token mint, org member, .. and %2e%2e traversal) with nothing sent; contents to default/guarded branch refused, on a new branch sent; author writes still sent; protection only strengthened; merge refused on a failed or missing required status, then merged with force_merge: false and head_commit_id once both succeed; protected repo not deleted; traversal through owner/repo refused. token.test.ts: scopes without write:admin, a kept admin-scoped token re-minted and the old one dropped, the scope re-mint on 403 still works.

merge-check.sh and merge-check-typescript.sh (with the toolchain's node_modules) pass on this head, which contains current main: 41 tests, 0 failed.

Based on main; independent of #140/#141 (neither touches `modules/gitea`). ## The route `gitea_api` (tools/index.ts) sent any method to any path with the tools' token, minted for the forge's site administrator `mesh-admin` with `write:repository` and `write:admin` (token.ts). An agent could drop a branch's protection or its required checks, set `mesh/merge-gate` to success through `/statuses`, write to `main` through `/contents`, make itself a collaborator or site admin, delete and recreate a repository the mesh builds from, or merge its own pull request past the checks. `gitea_branch_protection_set` could also weaken a rule directly. ## The fix (guard.ts, pure, tested without a forge) - **`gitea_api`** reads anything but `/admin` and `/users/*/tokens`; writes only what an author writes: issues, comments, labels, pull requests (opened, edited, reviewed — never merged), branches, and a file on a branch that is neither the default branch nor one a protection rule (or glob) names. Everything else is refused before it is sent. Paths with dot segments or encoded slashes/dots are refused. - **`gitea_merge_pull_request`** reads the base's protection and the head's statuses and merges only when every required status is a success (`mergeableUnder`, the same judge `gitea_commit_statuses` uses); sends `force_merge: false` and `head_commit_id`, so the forge refuses if the head moved after the check; refuses `manually-merged`. **The operator-approved merge stays**: after the operator's yes, the agent's call merges a pull request whose checks passed. - **`gitea_branch_protection_set`** only strengthens: dropping a required status, allowing direct push, or allowing an admin override is refused. - **`gitea_delete_repo`** refuses a repository any protection rule guards. - Every tool's `owner`/`repo` must be a name, never another path. - **The token is minted without `write:admin`.** Nothing in the module's code used it (the provisioner settles users with the admin login over basic auth, `GiteaAdmin`); it was there for hand calls through `gitea_api` (hq 229). Its scopes are now kept beside it, and a token kept with another list is minted again at the next start — the forge drops the old one by name, so the scope does not outlive this. ## What breaks - Settling the forge's users or teams by hand through `gitea_api` (hq 229's builder-as-site-admin was done that way) — now the operator's, at the forge as themselves, or a dedicated tool later. - Writing a file straight to `main` through `gitea_api` — use a branch and a pull request. - Deleting a protected repository through a tool. ## Not done: a non-admin account for the tools The token still belongs to `mesh-admin`, a site administrator, so scopes alone do not stop protection edits (they sit under `repository`) — the tools' guards do. Moving the tools to a non-admin user is feasible but larger: that user needs write access to every repository it merges and sets statuses on (a team in `novox`), the note append already runs in the forge's container, and creating a repository in an organisation needs membership. Proposed as a follow-up. ## Tests (`test/guard.test.ts`; each fails before, passes after) Against a fake forge: 15 refused `gitea_api` calls (admin, protection delete/patch, merge-gate status, merge with `force_merge`, collaborator, hook, key, repo settings, repo delete, transfer, token mint, org member, `..` and `%2e%2e` traversal) with nothing sent; contents to default/guarded branch refused, on a new branch sent; author writes still sent; protection only strengthened; merge refused on a failed or missing required status, then merged with `force_merge: false` and `head_commit_id` once both succeed; protected repo not deleted; traversal through `owner`/`repo` refused. `token.test.ts`: scopes without `write:admin`, a kept admin-scoped token re-minted and the old one dropped, the scope re-mint on 403 still works. `merge-check.sh` and `merge-check-typescript.sh` (with the toolchain's node_modules) pass on this head, which contains current main: 41 tests, 0 failed.
mesh-admin added 1 commit 2026-10-09 09:48:58 +00:00
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
080c113d69
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.
Author
Contributor

Delivery novox/mesh-catalog@1dca181e03e1 — superseded since 2026-10-09T11:53:57Z

Delivery plan — builds gitea → novox; no bus step

  • tier 0: gitea
  • novox: receives gitea
  • gitea provides npm-package-registry, git: its consumers are sent again after it
  • gitea keeps data (ADR 0233): what it holds is backed up before it moves

Machines

  • gitea on novox: judging — 0 healthy judging(s) so far

Transitions

  • 2026-10-09 11:42 (new) → proposed (announced): novox/mesh-catalog#153's head announced
  • 2026-10-09 11:44 proposed → checked (checked): the verdict names this commit
  • 2026-10-09 11:44 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
  • 2026-10-09 11:50 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
  • 2026-10-09 11:50 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path
  • 2026-10-09 11:53 delivering → superseded (superseded): a newer delivery to the same trunk took over its walk

The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.

<!-- mesh-delivery:view --> **Delivery** `novox/mesh-catalog@1dca181e03e1` — **superseded** since 2026-10-09T11:53:57Z **Delivery plan** — builds gitea → novox; no bus step - tier 0: gitea - novox: receives gitea - gitea provides npm-package-registry, git: its consumers are sent again after it - gitea keeps data (ADR 0233): what it holds is backed up before it moves **Machines** - gitea on novox: judging — 0 healthy judging(s) so far **Transitions** - 2026-10-09 11:42 (new) → proposed (announced): novox/mesh-catalog#153's head announced - 2026-10-09 11:44 proposed → checked (checked): the verdict names this commit - 2026-10-09 11:44 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail - 2026-10-09 11:50 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move - 2026-10-09 11:50 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path - 2026-10-09 11:53 delivering → superseded (superseded): a newer delivery to the same trunk took over its walk The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Author
Contributor

Merge check at 080c113d

  • mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea)
  • mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

Delivery plan — builds gitea → novox; no bus step

  • tier 0: gitea
  • novox: receives gitea
  • gitea provides npm-package-registry, git: its consumers are sent again after it
  • gitea keeps data (ADR 0233): what it holds is backed up before it moves

Run by the build seat on g14 as build-1791539340850985492 (builds --log build-1791539340850985492).

--- the gate: gitea
gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T08:07:39Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T08:07:39Z

modules the change moves:
  - gitea (changed)

machines:
  - a machine (f35): composes
  - holds mesh-dns-resolver (idt): composes
  - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
  - a machine (yvbpsm): composes

a merge would rebuild 1 module(s) in 1 tier(s): gitea
--- its merge-check.sh (go toolchain)
ok  	git.novox.be/novox/mesh-catalog/checks/words	0.002s
words: 12 retired words, none in the 34219 strings of 604 files
ok  	git.novox.be/novox/mesh-catalog/checks/paths	0.004s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 70, as kept
testing modules/gitea
ok  	gitea/cmd/npm-registry	1.714s
--- its merge-check-typescript.sh (typescript toolchain)
type-checking modules/gitea (TypeScript)
testing modules/gitea (TypeScript)
✔ the newest status of each context is kept, with what it said, when and by whom (22.395004ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.445484ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.2059ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.534473ms)
✔ a note line is appended once, as the forge's user, in its own repository (6.668372ms)
✔ a note that cannot be written is said, never read as written (0.375782ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.466584ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (2.935621ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (46.457227ms)
✔ gitea_api still writes what an author writes (6.418549ms)
✔ gitea_branch_protection_set only strengthens (16.151266ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (24.833113ms)
✔ gitea_delete_repo refuses a repository a protection rule guards (2.441765ms)
✔ a repository owner or name is never another path (0.320137ms)
✔ the judges, without a forge (0.322431ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (41.508001ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.33243ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (9.901487ms)
✔ a lookup the forge refuses is thrown, not read as no module (3.205482ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (4.410264ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.207273ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (4.080109ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.801448ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.569759ms)
✔ no verdict sets warning on a required check; only a person's decision fails (0.821335ms)
✔ a commit status is set on the commit, under the merge check's context (38.990061ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.373718ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (29.996353ms)
✔ second start: reuses the kept token, mints nothing (15.16616ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (18.447406ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (24.639877ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (18.335304ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (16.505297ms)
✔ concurrent first calls share one mint (10.486606ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (11.345472ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.819383ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (17.075066ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.167726ms)
✔ nothing to mint with and no token: the client says what is missing (0.145897ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-EuxjIx/state/token
✔ the tools register once there is a way to a token, and the first call mints it (6.688651ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-3qCtAM/state/token
✔ a pull request can be closed, read and cleaned up through the tools (20.232898ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 339.649728
**Merge check** at `080c113d` - `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea) - `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript) **Delivery plan** — builds gitea → novox; no bus step - tier 0: gitea - novox: receives gitea - gitea provides npm-package-registry, git: its consumers are sent again after it - gitea keeps data (ADR 0233): what it holds is backed up before it moves Run by the build seat on g14 as `build-1791539340850985492` (`builds --log build-1791539340850985492`). ``` --- the gate: gitea gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8) long-running resources without health: 1 files asking for a setting without saying whether it is trusted: 0 1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T08:07:39Z says they run composing every machine as the mesh is the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued composing every machine with the change the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose) judged by controller development build against the facts of 2026-10-09T08:07:39Z modules the change moves: - gitea (changed) machines: - a machine (f35): composes - holds mesh-dns-resolver (idt): composes - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes - a machine (yvbpsm): composes a merge would rebuild 1 module(s) in 1 tier(s): gitea --- its merge-check.sh (go toolchain) ok git.novox.be/novox/mesh-catalog/checks/words 0.002s words: 12 retired words, none in the 34219 strings of 604 files ok git.novox.be/novox/mesh-catalog/checks/paths 0.004s paths: no module names another's state on its machine (3 known, listed to go) long-running resources without health: 70, as kept testing modules/gitea ok gitea/cmd/npm-registry 1.714s --- its merge-check-typescript.sh (typescript toolchain) type-checking modules/gitea (TypeScript) testing modules/gitea (TypeScript) ✔ the newest status of each context is kept, with what it said, when and by whom (22.395004ms) ✔ a commit merges only when every required status is a success; a warning blocks (0.445484ms) ✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.2059ms) ✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.534473ms) ✔ a note line is appended once, as the forge's user, in its own repository (6.668372ms) ✔ a note that cannot be written is said, never read as written (0.375782ms) ✔ one view per pull request, found by its marker; only the mesh's statuses (0.466584ms) ✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (2.935621ms) ✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (46.457227ms) ✔ gitea_api still writes what an author writes (6.418549ms) ✔ gitea_branch_protection_set only strengthens (16.151266ms) ✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (24.833113ms) ✔ gitea_delete_repo refuses a repository a protection rule guards (2.441765ms) ✔ a repository owner or name is never another path (0.320137ms) ✔ the judges, without a forge (0.322431ms) ✔ every page of a pull request's files is read, though the forge caps a page at fifty (41.508001ms) ✔ a pass asks only the repositories that moved since the last look, every one before the first (0.33243ms) ✔ the directories above a merge's files that hold a module at the commit are said, the root never (9.901487ms) ✔ a lookup the forge refuses is thrown, not read as no module (3.205482ms) ✔ the statuses asked for replace the rule's, and an empty list requires none (4.410264ms) ✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.207273ms) ✔ each open pull request's new head is announced once, and nothing closed or merged (4.080109ms) ✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.801448ms) ✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.569759ms) ✔ no verdict sets warning on a required check; only a person's decision fails (0.821335ms) ✔ a commit status is set on the commit, under the merge check's context (38.990061ms) ✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.373718ms) ✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (29.996353ms) ✔ second start: reuses the kept token, mints nothing (15.16616ms) ✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (18.447406ms) ✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (24.639877ms) ✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (18.335304ms) ✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (16.505297ms) ✔ concurrent first calls share one mint (10.486606ms) ✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (11.345472ms) ✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.819383ms) ✔ a second process finds the token the first renewed, and reuses it instead of minting over it (17.075066ms) ✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.167726ms) ✔ nothing to mint with and no token: the client says what is missing (0.145897ms) [gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-EuxjIx/state/token ✔ the tools register once there is a way to a token, and the first call mints it (6.688651ms) [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-3qCtAM/state/token ✔ a pull request can be closed, read and cleaned up through the tools (20.232898ms) ℹ tests 41 ℹ suites 0 ℹ pass 41 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 339.649728 ```
jschoubben added 1 commit 2026-10-09 10:26:43 +00:00
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
ff648ad32b
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.
Author
Contributor

Follow-ups of the review, commit ff648ad:

  • gitea_api sends the normalised path it judged (its query kept), never the raw path.
  • gitea_api refuses PATCH /repos/o/r/pulls/{n} with a base in the body. gitea_update_pull_request never changes a base.
  • gitea_merge_pull_request merges only into a branch whose rule requires status checks. Before, a pull request whose base was moved to an unguarded branch would merge with "nothing required".
  • gitea_branch_protection_set: a rule named for a branch that has none of its own is judged against every glob rule covering it, with the forge's defaults for a new rule (pushes refused, admin override allowed). It may not be weaker than any of them.
  • What I chose for gitea_delete_repo: the forge tools have no terminal, so it deletes only when all of these hold: the repository belongs to the tools' own user (from GET /user), no protection rule guards it, it has no commit in 30 days, and confirm is exactly owner/name. An organisation's or a person's repository is never deleted by a tool. The mesh builds only from those, so 'any repository a module builds from' is covered without the gitea module needing the catalogue.

Four new tests, each failing on the previous head; 44 pass, 0 fail.

Follow-ups of the review, commit ff648ad: - `gitea_api` sends the normalised path it judged (its query kept), never the raw path. - `gitea_api` refuses `PATCH /repos/o/r/pulls/{n}` with a `base` in the body. `gitea_update_pull_request` never changes a base. - `gitea_merge_pull_request` merges only into a branch whose rule requires status checks. Before, a pull request whose base was moved to an unguarded branch would merge with "nothing required". - `gitea_branch_protection_set`: a rule named for a branch that has none of its own is judged against every glob rule covering it, with the forge's defaults for a new rule (pushes refused, admin override allowed). It may not be weaker than any of them. - **What I chose for `gitea_delete_repo`:** the forge tools have no terminal, so it deletes only when all of these hold: the repository belongs to the tools' own user (from `GET /user`), no protection rule guards it, it has no commit in 30 days, and `confirm` is exactly `owner/name`. An organisation's or a person's repository is never deleted by a tool. The mesh builds only from those, so 'any repository a module builds from' is covered without the gitea module needing the catalogue. Four new tests, each failing on the previous head; 44 pass, 0 fail.
Author
Contributor

Merge check at ff648ad3

  • mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea)
  • mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

Delivery plan — builds gitea → novox; no bus step

  • tier 0: gitea
  • novox: receives gitea
  • gitea provides npm-package-registry, git: its consumers are sent again after it
  • gitea keeps data (ADR 0233): what it holds is backed up before it moves

Run by the build seat on ace as build-1791541623901758334 (builds --log build-1791541623901758334).

--- the gate: gitea
gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T08:07:39Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T08:07:39Z

modules the change moves:
  - gitea (changed)

machines:
  - a machine (f35): composes
  - holds mesh-dns-resolver (idt): composes
  - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
  - a machine (yvbpsm): composes

a merge would rebuild 1 module(s) in 1 tier(s): gitea
--- its merge-check.sh (go toolchain)
ok  	git.novox.be/novox/mesh-catalog/checks/words	0.002s
words: 12 retired words, none in the 34237 strings of 604 files
ok  	git.novox.be/novox/mesh-catalog/checks/paths	0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 70, as kept
testing modules/gitea
ok  	gitea/cmd/npm-registry	1.701s
--- its merge-check-typescript.sh (typescript toolchain)
type-checking modules/gitea (TypeScript)
testing modules/gitea (TypeScript)
✔ the newest status of each context is kept, with what it said, when and by whom (18.294011ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.432672ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.191308ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.572644ms)
✔ a note line is appended once, as the forge's user, in its own repository (6.326605ms)
✔ a note that cannot be written is said, never read as written (0.361708ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.639659ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (3.905825ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (41.441244ms)
✔ gitea_api still writes what an author writes (7.008024ms)
✔ gitea_branch_protection_set only strengthens (14.159146ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (25.793948ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (22.975321ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (3.960518ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (7.21429ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (12.945509ms)
✔ a repository owner or name is never another path (0.368732ms)
✔ the judges, without a forge (0.295344ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (23.503252ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.343505ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (10.5791ms)
✔ a lookup the forge refuses is thrown, not read as no module (3.008653ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (4.678074ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.256311ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (3.135571ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.607359ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.472887ms)
✔ no verdict sets warning on a required check; only a person's decision fails (0.643186ms)
✔ a commit status is set on the commit, under the merge check's context (28.169453ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.417824ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (30.255175ms)
✔ second start: reuses the kept token, mints nothing (15.49418ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (17.947942ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (18.606557ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (17.251666ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (15.785014ms)
✔ concurrent first calls share one mint (9.471685ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (12.456652ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.928151ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (19.067031ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.242916ms)
✔ nothing to mint with and no token: the client says what is missing (0.184957ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-sBGppV/state/token
✔ the tools register once there is a way to a token, and the first call mints it (8.308633ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-YaK3ZJ/state/token
✔ a pull request can be closed, read and cleaned up through the tools (19.580153ms)
ℹ tests 44
ℹ suites 0
ℹ pass 44
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 304.70973
**Merge check** at `ff648ad3` - `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea) - `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript) **Delivery plan** — builds gitea → novox; no bus step - tier 0: gitea - novox: receives gitea - gitea provides npm-package-registry, git: its consumers are sent again after it - gitea keeps data (ADR 0233): what it holds is backed up before it moves Run by the build seat on ace as `build-1791541623901758334` (`builds --log build-1791541623901758334`). ``` --- the gate: gitea gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8) long-running resources without health: 1 files asking for a setting without saying whether it is trusted: 0 1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T08:07:39Z says they run composing every machine as the mesh is the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued composing every machine with the change the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose) judged by controller development build against the facts of 2026-10-09T08:07:39Z modules the change moves: - gitea (changed) machines: - a machine (f35): composes - holds mesh-dns-resolver (idt): composes - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes - a machine (yvbpsm): composes a merge would rebuild 1 module(s) in 1 tier(s): gitea --- its merge-check.sh (go toolchain) ok git.novox.be/novox/mesh-catalog/checks/words 0.002s words: 12 retired words, none in the 34237 strings of 604 files ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s paths: no module names another's state on its machine (3 known, listed to go) long-running resources without health: 70, as kept testing modules/gitea ok gitea/cmd/npm-registry 1.701s --- its merge-check-typescript.sh (typescript toolchain) type-checking modules/gitea (TypeScript) testing modules/gitea (TypeScript) ✔ the newest status of each context is kept, with what it said, when and by whom (18.294011ms) ✔ a commit merges only when every required status is a success; a warning blocks (0.432672ms) ✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.191308ms) ✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.572644ms) ✔ a note line is appended once, as the forge's user, in its own repository (6.326605ms) ✔ a note that cannot be written is said, never read as written (0.361708ms) ✔ one view per pull request, found by its marker; only the mesh's statuses (0.639659ms) ✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (3.905825ms) ✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (41.441244ms) ✔ gitea_api still writes what an author writes (7.008024ms) ✔ gitea_branch_protection_set only strengthens (14.159146ms) ✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (25.793948ms) ✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (22.975321ms) ✔ gitea_api sends the path it judged, and refuses moving a pull request's base (3.960518ms) ✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (7.21429ms) ✔ a rule named for a branch is not made weaker than the glob rule that guards it now (12.945509ms) ✔ a repository owner or name is never another path (0.368732ms) ✔ the judges, without a forge (0.295344ms) ✔ every page of a pull request's files is read, though the forge caps a page at fifty (23.503252ms) ✔ a pass asks only the repositories that moved since the last look, every one before the first (0.343505ms) ✔ the directories above a merge's files that hold a module at the commit are said, the root never (10.5791ms) ✔ a lookup the forge refuses is thrown, not read as no module (3.008653ms) ✔ the statuses asked for replace the rule's, and an empty list requires none (4.678074ms) ✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.256311ms) ✔ each open pull request's new head is announced once, and nothing closed or merged (3.135571ms) ✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.607359ms) ✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.472887ms) ✔ no verdict sets warning on a required check; only a person's decision fails (0.643186ms) ✔ a commit status is set on the commit, under the merge check's context (28.169453ms) ✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.417824ms) ✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (30.255175ms) ✔ second start: reuses the kept token, mints nothing (15.49418ms) ✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (17.947942ms) ✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (18.606557ms) ✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (17.251666ms) ✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (15.785014ms) ✔ concurrent first calls share one mint (9.471685ms) ✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (12.456652ms) ✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.928151ms) ✔ a second process finds the token the first renewed, and reuses it instead of minting over it (19.067031ms) ✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.242916ms) ✔ nothing to mint with and no token: the client says what is missing (0.184957ms) [gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-sBGppV/state/token ✔ the tools register once there is a way to a token, and the first call mints it (8.308633ms) [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-YaK3ZJ/state/token ✔ a pull request can be closed, read and cleaned up through the tools (19.580153ms) ℹ tests 44 ℹ suites 0 ℹ pass 44 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 304.70973 ```
jschoubben added 2 commits 2026-10-09 10:57:26 +00:00
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.
Merge remote-tracking branch 'origin/main' into fix/the-forge-tools-never-weaken-protection
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
213950e334
Author
Contributor

Merge check at 213950e3

  • mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea)
  • mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

Delivery plan — builds gitea → novox; no bus step

  • tier 0: gitea
  • novox: receives gitea
  • gitea provides npm-package-registry, git: its consumers are sent again after it
  • gitea keeps data (ADR 0233): what it holds is backed up before it moves

Run by the build seat on ace as build-1791543453554850947 (builds --log build-1791543453554850947).

--- the gate: gitea
gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T08:07:39Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T08:07:39Z

modules the change moves:
  - gitea (changed)

machines:
  - a machine (f35): composes
  - holds mesh-dns-resolver (idt): composes
  - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
  - a machine (yvbpsm): composes

a merge would rebuild 1 module(s) in 1 tier(s): gitea
--- its merge-check.sh (go toolchain)
ok  	git.novox.be/novox/mesh-catalog/checks/words	0.002s
words: 12 retired words, none in the 35057 strings of 611 files
ok  	git.novox.be/novox/mesh-catalog/checks/paths	0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 70, as kept
testing modules/gitea
ok  	gitea/cmd/npm-registry	1.661s
--- its merge-check-typescript.sh (typescript toolchain)
type-checking modules/gitea (TypeScript)
testing modules/gitea (TypeScript)
✔ the newest status of each context is kept, with what it said, when and by whom (16.047687ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.551547ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.22871ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.687182ms)
✔ a note line is appended once, as the forge's user, in its own repository (5.60689ms)
✔ a note that cannot be written is said, never read as written (0.299253ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.488699ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (4.000942ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (40.353611ms)
✔ gitea_api still writes what an author writes (7.348504ms)
✔ gitea_branch_protection_set only strengthens (11.998938ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (25.93749ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (23.00704ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.572006ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (6.238396ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (12.863733ms)
✔ a repository owner or name is never another path (0.370076ms)
✔ the judges, without a forge (0.303742ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (2.539134ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (2.355217ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (21.167584ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.3244ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (8.992354ms)
✔ a lookup the forge refuses is thrown, not read as no module (2.676612ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (4.699135ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.22346ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (3.292681ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.587594ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.465425ms)
✔ no verdict sets warning on a required check; only a person's decision fails (0.63314ms)
✔ a commit status is set on the commit, under the merge check's context (25.180278ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.332906ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (25.098954ms)
✔ second start: reuses the kept token, mints nothing (12.448602ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (18.009616ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (16.658465ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (14.344176ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (14.723028ms)
✔ concurrent first calls share one mint (9.055493ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (10.281718ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (1.032721ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (17.323046ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.192581ms)
✔ nothing to mint with and no token: the client says what is missing (0.199966ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-VdkrFb/state/token
✔ the tools register once there is a way to a token, and the first call mints it (8.256974ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-dn8Gw8/state/token
✔ a pull request can be closed, read and cleaned up through the tools (18.376475ms)
ℹ tests 46
ℹ suites 0
ℹ pass 46
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 279.780171
**Merge check** at `213950e3` - `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea) - `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript) **Delivery plan** — builds gitea → novox; no bus step - tier 0: gitea - novox: receives gitea - gitea provides npm-package-registry, git: its consumers are sent again after it - gitea keeps data (ADR 0233): what it holds is backed up before it moves Run by the build seat on ace as `build-1791543453554850947` (`builds --log build-1791543453554850947`). ``` --- the gate: gitea gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8) long-running resources without health: 1 files asking for a setting without saying whether it is trusted: 0 1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T08:07:39Z says they run composing every machine as the mesh is the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued composing every machine with the change the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose) judged by controller development build against the facts of 2026-10-09T08:07:39Z modules the change moves: - gitea (changed) machines: - a machine (f35): composes - holds mesh-dns-resolver (idt): composes - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes - a machine (yvbpsm): composes a merge would rebuild 1 module(s) in 1 tier(s): gitea --- its merge-check.sh (go toolchain) ok git.novox.be/novox/mesh-catalog/checks/words 0.002s words: 12 retired words, none in the 35057 strings of 611 files ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s paths: no module names another's state on its machine (3 known, listed to go) long-running resources without health: 70, as kept testing modules/gitea ok gitea/cmd/npm-registry 1.661s --- its merge-check-typescript.sh (typescript toolchain) type-checking modules/gitea (TypeScript) testing modules/gitea (TypeScript) ✔ the newest status of each context is kept, with what it said, when and by whom (16.047687ms) ✔ a commit merges only when every required status is a success; a warning blocks (0.551547ms) ✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.22871ms) ✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.687182ms) ✔ a note line is appended once, as the forge's user, in its own repository (5.60689ms) ✔ a note that cannot be written is said, never read as written (0.299253ms) ✔ one view per pull request, found by its marker; only the mesh's statuses (0.488699ms) ✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (4.000942ms) ✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (40.353611ms) ✔ gitea_api still writes what an author writes (7.348504ms) ✔ gitea_branch_protection_set only strengthens (11.998938ms) ✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (25.93749ms) ✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (23.00704ms) ✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.572006ms) ✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (6.238396ms) ✔ a rule named for a branch is not made weaker than the glob rule that guards it now (12.863733ms) ✔ a repository owner or name is never another path (0.370076ms) ✔ the judges, without a forge (0.303742ms) ✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (2.539134ms) ✔ gitea_api never approves a pull request nor requests changes: those are the operator's (2.355217ms) ✔ every page of a pull request's files is read, though the forge caps a page at fifty (21.167584ms) ✔ a pass asks only the repositories that moved since the last look, every one before the first (0.3244ms) ✔ the directories above a merge's files that hold a module at the commit are said, the root never (8.992354ms) ✔ a lookup the forge refuses is thrown, not read as no module (2.676612ms) ✔ the statuses asked for replace the rule's, and an empty list requires none (4.699135ms) ✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.22346ms) ✔ each open pull request's new head is announced once, and nothing closed or merged (3.292681ms) ✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.587594ms) ✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.465425ms) ✔ no verdict sets warning on a required check; only a person's decision fails (0.63314ms) ✔ a commit status is set on the commit, under the merge check's context (25.180278ms) ✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.332906ms) ✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (25.098954ms) ✔ second start: reuses the kept token, mints nothing (12.448602ms) ✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (18.009616ms) ✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (16.658465ms) ✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (14.344176ms) ✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (14.723028ms) ✔ concurrent first calls share one mint (9.055493ms) ✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (10.281718ms) ✔ one process shares one source per kept file — the watcher and the tools never renew against each other (1.032721ms) ✔ a second process finds the token the first renewed, and reuses it instead of minting over it (17.323046ms) ✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.192581ms) ✔ nothing to mint with and no token: the client says what is missing (0.199966ms) [gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-VdkrFb/state/token ✔ the tools register once there is a way to a token, and the first call mints it (8.256974ms) [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-dn8Gw8/state/token ✔ a pull request can be closed, read and cleaned up through the tools (18.376475ms) ℹ tests 46 ℹ suites 0 ℹ pass 46 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 279.780171 ```
Author
Contributor

Urgent fix from the confirmation review, commit ea772a9; head 213950e after merging main (no conflicts, nothing under modules/gitea changed on main).

  • No acting as another user. gitea_api now sends only an allow-list of query keys (QUERY_KEYS in guard.ts: limit, page, state, type, q, sort, order, since, before, ref, sha, path, labels, milestones, keyword, stat, verification, files, not, mode, include_desc, archived, private, exclusive, uid, priority_repo_id, created_by, assigned_by, mentioned_by, whitespace, binary). sudo in any case is refused, and so is any other key, token and access_token included. The query is judged and also rebuilt from the allow-list before sending (sentQuery), so what is sent is what was judged.
  • No Sudo header. gitea_api takes no headers argument, and the client sets only Content-Type and Authorization. There is no header route.
  • No approvals. A POST /repos/o/r/pulls/N/reviews (or /reviews/{id}) whose event is anything but COMMENT or PENDING (APPROVE, REQUEST_CHANGES, any case) is refused. Approving is the operator's, at the forge.

Tests: gitea_api never acts as another user covers ?sudo=, ?SUDO=, Sudo beside allowed keys, token, access_token, and a sudo comment post. Nothing reaches the fake forge, and an allowed query arrives unchanged. gitea_api never approves a pull request nor requests changes covers APPROVE, approve, REQUEST_CHANGES and APPROVE on a pending review; a COMMENT review is still sent. Both fail on the previous head. 46 pass, 0 fail.

Urgent fix from the confirmation review, commit ea772a9; head 213950e after merging main (no conflicts, nothing under `modules/gitea` changed on main). - **No acting as another user.** `gitea_api` now sends only an allow-list of query keys (`QUERY_KEYS` in guard.ts: limit, page, state, type, q, sort, order, since, before, ref, sha, path, labels, milestones, keyword, stat, verification, files, not, mode, include_desc, archived, private, exclusive, uid, priority_repo_id, created_by, assigned_by, mentioned_by, whitespace, binary). `sudo` in any case is refused, and so is any other key, `token` and `access_token` included. The query is judged and also rebuilt from the allow-list before sending (`sentQuery`), so what is sent is what was judged. - **No Sudo header.** `gitea_api` takes no headers argument, and the client sets only `Content-Type` and `Authorization`. There is no header route. - **No approvals.** A `POST /repos/o/r/pulls/N/reviews` (or `/reviews/{id}`) whose `event` is anything but COMMENT or PENDING (APPROVE, REQUEST_CHANGES, any case) is refused. Approving is the operator's, at the forge. Tests: `gitea_api never acts as another user` covers `?sudo=`, `?SUDO=`, `Sudo` beside allowed keys, `token`, `access_token`, and a sudo comment post. Nothing reaches the fake forge, and an allowed query arrives unchanged. `gitea_api never approves a pull request nor requests changes` covers APPROVE, approve, REQUEST_CHANGES and APPROVE on a pending review; a COMMENT review is still sent. Both fail on the previous head. 46 pass, 0 fail.
jschoubben added 1 commit 2026-10-09 11:35:21 +00:00
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
5eab586116
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.
Author
Contributor

Merge check at 5eab5861

  • mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea)
  • mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

Delivery plan — builds gitea → novox; no bus step

  • tier 0: gitea
  • novox: receives gitea
  • gitea provides npm-package-registry, git: its consumers are sent again after it
  • gitea keeps data (ADR 0233): what it holds is backed up before it moves

Run by the build seat on ace as build-1791545730442225160 (builds --log build-1791545730442225160).

--- the gate: gitea
gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T11:25:23Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T11:25:23Z

modules the change moves:
  - gitea (changed)

machines:
  - a machine (f35): composes
  - holds mesh-dns-resolver (idt): composes
  - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
  - a machine (yvbpsm): composes

a merge would rebuild 1 module(s) in 1 tier(s): gitea
--- its merge-check.sh (go toolchain)
ok  	git.novox.be/novox/mesh-catalog/checks/words	0.002s
words: 12 retired words, none in the 35079 strings of 611 files
ok  	git.novox.be/novox/mesh-catalog/checks/paths	0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 70, as kept
testing modules/gitea
ok  	gitea/cmd/npm-registry	1.682s
--- its merge-check-typescript.sh (typescript toolchain)
type-checking modules/gitea (TypeScript)
testing modules/gitea (TypeScript)
✔ the newest status of each context is kept, with what it said, when and by whom (9.904341ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.413719ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.192753ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.58973ms)
✔ a note line is appended once, as the forge's user, in its own repository (5.304364ms)
✔ a note that cannot be written is said, never read as written (0.303872ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.460697ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (4.196519ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (38.915491ms)
✔ gitea_api still writes what an author writes (6.245564ms)
✔ gitea_branch_protection_set only strengthens (11.429282ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (27.263321ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (23.363332ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.579509ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (7.219297ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (14.465917ms)
✔ a repository owner or name is never another path (0.360268ms)
✔ the judges, without a forge (0.471367ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (2.191474ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (2.942547ms)
✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (3.616516ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (18.931992ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.315454ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (9.056965ms)
✔ a lookup the forge refuses is thrown, not read as no module (2.751489ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (4.327735ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.220304ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (3.052304ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.599729ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.490853ms)
✔ no verdict sets warning on a required check; only a person's decision fails (0.654291ms)
✔ a commit status is set on the commit, under the merge check's context (26.888998ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.36141ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (25.884978ms)
✔ second start: reuses the kept token, mints nothing (15.786351ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (15.864828ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (19.570914ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (17.1248ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (15.673008ms)
✔ concurrent first calls share one mint (9.278553ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (11.134585ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.835171ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (18.063417ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.268449ms)
✔ nothing to mint with and no token: the client says what is missing (0.177394ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-AIy3Dx/state/token
✔ the tools register once there is a way to a token, and the first call mints it (8.151081ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-j4EG0I/state/token
✔ a pull request can be closed, read and cleaned up through the tools (18.942301ms)
ℹ tests 47
ℹ suites 0
ℹ pass 47
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 292.430615
**Merge check** at `5eab5861` - `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea) - `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript) **Delivery plan** — builds gitea → novox; no bus step - tier 0: gitea - novox: receives gitea - gitea provides npm-package-registry, git: its consumers are sent again after it - gitea keeps data (ADR 0233): what it holds is backed up before it moves Run by the build seat on ace as `build-1791545730442225160` (`builds --log build-1791545730442225160`). ``` --- the gate: gitea gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8) long-running resources without health: 1 files asking for a setting without saying whether it is trusted: 0 1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T11:25:23Z says they run composing every machine as the mesh is the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued composing every machine with the change the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose) judged by controller development build against the facts of 2026-10-09T11:25:23Z modules the change moves: - gitea (changed) machines: - a machine (f35): composes - holds mesh-dns-resolver (idt): composes - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes - a machine (yvbpsm): composes a merge would rebuild 1 module(s) in 1 tier(s): gitea --- its merge-check.sh (go toolchain) ok git.novox.be/novox/mesh-catalog/checks/words 0.002s words: 12 retired words, none in the 35079 strings of 611 files ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s paths: no module names another's state on its machine (3 known, listed to go) long-running resources without health: 70, as kept testing modules/gitea ok gitea/cmd/npm-registry 1.682s --- its merge-check-typescript.sh (typescript toolchain) type-checking modules/gitea (TypeScript) testing modules/gitea (TypeScript) ✔ the newest status of each context is kept, with what it said, when and by whom (9.904341ms) ✔ a commit merges only when every required status is a success; a warning blocks (0.413719ms) ✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.192753ms) ✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.58973ms) ✔ a note line is appended once, as the forge's user, in its own repository (5.304364ms) ✔ a note that cannot be written is said, never read as written (0.303872ms) ✔ one view per pull request, found by its marker; only the mesh's statuses (0.460697ms) ✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (4.196519ms) ✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (38.915491ms) ✔ gitea_api still writes what an author writes (6.245564ms) ✔ gitea_branch_protection_set only strengthens (11.429282ms) ✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (27.263321ms) ✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (23.363332ms) ✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.579509ms) ✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (7.219297ms) ✔ a rule named for a branch is not made weaker than the glob rule that guards it now (14.465917ms) ✔ a repository owner or name is never another path (0.360268ms) ✔ the judges, without a forge (0.471367ms) ✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (2.191474ms) ✔ gitea_api never approves a pull request nor requests changes: those are the operator's (2.942547ms) ✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (3.616516ms) ✔ every page of a pull request's files is read, though the forge caps a page at fifty (18.931992ms) ✔ a pass asks only the repositories that moved since the last look, every one before the first (0.315454ms) ✔ the directories above a merge's files that hold a module at the commit are said, the root never (9.056965ms) ✔ a lookup the forge refuses is thrown, not read as no module (2.751489ms) ✔ the statuses asked for replace the rule's, and an empty list requires none (4.327735ms) ✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.220304ms) ✔ each open pull request's new head is announced once, and nothing closed or merged (3.052304ms) ✔ a verdict is the head commit's status; an error is the forge's error, never a success (0.599729ms) ✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.490853ms) ✔ no verdict sets warning on a required check; only a person's decision fails (0.654291ms) ✔ a commit status is set on the commit, under the merge check's context (26.888998ms) ✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.36141ms) ✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (25.884978ms) ✔ second start: reuses the kept token, mints nothing (15.786351ms) ✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (15.864828ms) ✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (19.570914ms) ✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (17.1248ms) ✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (15.673008ms) ✔ concurrent first calls share one mint (9.278553ms) ✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (11.134585ms) ✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.835171ms) ✔ a second process finds the token the first renewed, and reuses it instead of minting over it (18.063417ms) ✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.268449ms) ✔ nothing to mint with and no token: the client says what is missing (0.177394ms) [gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-AIy3Dx/state/token ✔ the tools register once there is a way to a token, and the first call mints it (8.151081ms) [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-j4EG0I/state/token ✔ a pull request can be closed, read and cleaned up through the tools (18.942301ms) ℹ tests 47 ℹ suites 0 ℹ pass 47 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 292.430615 ```
jschoubben added 2 commits 2026-10-09 11:41:41 +00:00
# Conflicts:
#	modules/gitea/client.ts
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
1dca181e03
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.
Author
Contributor

Merge check at 1dca181e

  • mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea)
  • mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

Delivery plan — builds gitea → novox; no bus step

  • tier 0: gitea
  • novox: receives gitea
  • gitea provides npm-package-registry, git: its consumers are sent again after it
  • gitea keeps data (ADR 0233): what it holds is backed up before it moves

Run by the build seat on novox as build-1791546122974779924 (builds --log build-1791546122974779924).

--- the gate: gitea
gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T11:35:23Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T11:35:23Z

modules the change moves:
  - gitea (changed)

machines:
  - a machine (f35): composes
  - holds mesh-dns-resolver (idt): composes
  - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
  - a machine (yvbpsm): composes

a merge would rebuild 1 module(s) in 1 tier(s): gitea
--- its merge-check.sh (go toolchain)
ok  	git.novox.be/novox/mesh-catalog/checks/words	0.002s
words: 12 retired words, none in the 35146 strings of 614 files
ok  	git.novox.be/novox/mesh-catalog/checks/paths	0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 70, as kept
testing modules/gitea
ok  	gitea/cmd/npm-registry	1.930s
--- its merge-check-typescript.sh (typescript toolchain)
type-checking modules/gitea (TypeScript)
testing modules/gitea (TypeScript)
✔ a pull request opened after the margin is looked at, by its own update time (17.225392ms)
✔ every open pull request is read with what its base requires and what its head has (gitea_open_pulls) (66.063335ms)
✔ a pull request or issue number that is not one is refused before the forge is asked (2.014232ms)
✔ the newest status of each context is kept, with what it said, when and by whom (14.473269ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.448311ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.27057ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.672421ms)
✔ a note line is appended once, as the forge's user, in its own repository (6.944759ms)
✔ a note that cannot be written is said, never read as written (0.454791ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.651641ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (5.215167ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (51.091356ms)
✔ gitea_api still writes what an author writes (8.13464ms)
✔ gitea_branch_protection_set only strengthens (16.443661ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (38.47339ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (25.837233ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.411596ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (6.207418ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (15.213499ms)
✔ a repository owner or name is never another path (0.46106ms)
✔ the judges, without a forge (0.395251ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (2.072142ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (1.919163ms)
✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (4.754006ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (24.483591ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.476071ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (16.165421ms)
✔ a lookup the forge refuses is thrown, not read as no module (4.412625ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (7.074529ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.254071ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (12.568176ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (1.659782ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.776701ms)
✔ no verdict sets warning on a required check; only a person's decision fails (1.068321ms)
✔ a commit status is set on the commit, under the merge check's context (38.025939ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.611591ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (37.532248ms)
✔ second start: reuses the kept token, mints nothing (18.525993ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (23.52781ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (24.424911ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (21.844408ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (16.481561ms)
✔ concurrent first calls share one mint (10.135453ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (13.588528ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.894331ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (18.439434ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (3.019914ms)
✔ nothing to mint with and no token: the client says what is missing (0.18342ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-AsjSO5/state/token
✔ the tools register once there is a way to a token, and the first call mints it (7.65703ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-pFPnEL/state/token
✔ a pull request can be closed, read and cleaned up through the tools (19.390365ms)
ℹ tests 50
ℹ suites 0
ℹ pass 50
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 417.429684
**Merge check** at `1dca181e` - `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: gitea) - `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript) **Delivery plan** — builds gitea → novox; no bus step - tier 0: gitea - novox: receives gitea - gitea provides npm-package-registry, git: its consumers are sent again after it - gitea keeps data (ADR 0233): what it holds is backed up before it moves Run by the build seat on novox as `build-1791546122974779924` (`builds --log build-1791546122974779924`). ``` --- the gate: gitea gitea: ok, keeps data (valuable); WARNING: server stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8) long-running resources without health: 1 files asking for a setting without saying whether it is trusted: 0 1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T11:35:23Z says they run composing every machine as the mesh is the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued composing every machine with the change the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose) judged by controller development build against the facts of 2026-10-09T11:35:23Z modules the change moves: - gitea (changed) machines: - a machine (f35): composes - holds mesh-dns-resolver (idt): composes - holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes - a machine (yvbpsm): composes a merge would rebuild 1 module(s) in 1 tier(s): gitea --- its merge-check.sh (go toolchain) ok git.novox.be/novox/mesh-catalog/checks/words 0.002s words: 12 retired words, none in the 35146 strings of 614 files ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s paths: no module names another's state on its machine (3 known, listed to go) long-running resources without health: 70, as kept testing modules/gitea ok gitea/cmd/npm-registry 1.930s --- its merge-check-typescript.sh (typescript toolchain) type-checking modules/gitea (TypeScript) testing modules/gitea (TypeScript) ✔ a pull request opened after the margin is looked at, by its own update time (17.225392ms) ✔ every open pull request is read with what its base requires and what its head has (gitea_open_pulls) (66.063335ms) ✔ a pull request or issue number that is not one is refused before the forge is asked (2.014232ms) ✔ the newest status of each context is kept, with what it said, when and by whom (14.473269ms) ✔ a commit merges only when every required status is a success; a warning blocks (0.448311ms) ✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.27057ms) ✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.672421ms) ✔ a note line is appended once, as the forge's user, in its own repository (6.944759ms) ✔ a note that cannot be written is said, never read as written (0.454791ms) ✔ one view per pull request, found by its marker; only the mesh's statuses (0.651641ms) ✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (5.215167ms) ✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (51.091356ms) ✔ gitea_api still writes what an author writes (8.13464ms) ✔ gitea_branch_protection_set only strengthens (16.443661ms) ✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (38.47339ms) ✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (25.837233ms) ✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.411596ms) ✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (6.207418ms) ✔ a rule named for a branch is not made weaker than the glob rule that guards it now (15.213499ms) ✔ a repository owner or name is never another path (0.46106ms) ✔ the judges, without a forge (0.395251ms) ✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (2.072142ms) ✔ gitea_api never approves a pull request nor requests changes: those are the operator's (1.919163ms) ✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (4.754006ms) ✔ every page of a pull request's files is read, though the forge caps a page at fifty (24.483591ms) ✔ a pass asks only the repositories that moved since the last look, every one before the first (0.476071ms) ✔ the directories above a merge's files that hold a module at the commit are said, the root never (16.165421ms) ✔ a lookup the forge refuses is thrown, not read as no module (4.412625ms) ✔ the statuses asked for replace the rule's, and an empty list requires none (7.074529ms) ✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.254071ms) ✔ each open pull request's new head is announced once, and nothing closed or merged (12.568176ms) ✔ a verdict is the head commit's status; an error is the forge's error, never a success (1.659782ms) ✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.776701ms) ✔ no verdict sets warning on a required check; only a person's decision fails (1.068321ms) ✔ a commit status is set on the commit, under the merge check's context (38.025939ms) ✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.611591ms) ✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (37.532248ms) ✔ second start: reuses the kept token, mints nothing (18.525993ms) ✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (23.52781ms) ✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (24.424911ms) ✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (21.844408ms) ✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (16.481561ms) ✔ concurrent first calls share one mint (10.135453ms) ✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (13.588528ms) ✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.894331ms) ✔ a second process finds the token the first renewed, and reuses it instead of minting over it (18.439434ms) ✔ a configured token wins, and is reported rather than minted over when the forge rejects it (3.019914ms) ✔ nothing to mint with and no token: the client says what is missing (0.18342ms) [gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-AsjSO5/state/token ✔ the tools register once there is a way to a token, and the first call mints it (7.65703ms) [gitea] no token kept — minting one [gitea] the forge already holds a token named "mesh-tools" — replacing it [gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-pFPnEL/state/token ✔ a pull request can be closed, read and cleaned up through the tools (19.390365ms) ℹ tests 50 ℹ suites 0 ℹ pass 50 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 417.429684 ```
Author
Contributor

Path fix from the confirmation review: commit 5eab586, then main merged in with #159 (merge 46fe6ff), then 1dca181.

  • One helper per caller segment (pathSegment / pathSegments in guard.ts). It refuses an empty segment, ., .., and any of / ? # % \ or control characters, before encoding.
    • pathSegments judges each part on its own, so a branch like feat/x or a file path still works.
    • Used for: deleteBranch, setBranchProtection (PATCH), getFile (per segment), moduleDirsAt/holdsFile, combinedStatus, and every /repos/<owner>/<name> through repoPath. That last one covers gitea_open_pulls (#159), whose names come from the forge's own answer.
    • gitea_branch_protection_set also judges the rule's branch name, so a rule named .. is never created.
  • Commits checked. setCommitStatus, commitStatuses and compare take only a commit (isCommit).
  • Checked again at send. GiteaClient.send runs checkSentPath on every request: no empty or dot segment, encoded or not, no fragment, and only the allow-listed query keys. It covers any path built elsewhere too.
  • Every interpolated path in the client was grepped.
    • What is left: numbers (index goes through #159's indexOf, comment ids are numbers), URLSearchParams built from fixed keys, and the GiteaAdmin class.
    • GiteaAdmin builds paths with encodeURIComponent (or team ids) from the provisioner's manifest settings. It has no tool, so no caller reaches it.
  • Merge resolution. I kept #159's indexOf and this PR's mergePullRequest(…, headCommit). Three test fixtures used 6-character shas (abc123, aaa111, bbb222); a commit is 7 to 40 characters, so they are now 7.

Test: no tool's caller text becomes another path or another user. These 13 cases are refused, with only a read of the repository's own rules reaching the fake forge:

  • gitea_delete_branch with .., feat/../.., x?sudo= and %2e%2e
  • gitea_get_file with ../../../../version and README.md?sudo=
  • gitea_commit_status with abc1234?sudo= and ../../../admin/users
  • gitea_commit_statuses with .. and main?sudo=
  • gitea_branch_protection_set with .. and main?sudo=
  • gitea_contains with ..

A branch feat/x and a file in a directory still go through. On the previous head the test fails at the first case: gitea_delete_branch("..") reached the forge. 50 pass, 0 fail.

Path fix from the confirmation review: commit 5eab586, then main merged in with #159 (merge 46fe6ff), then 1dca181. - **One helper per caller segment** (`pathSegment` / `pathSegments` in guard.ts). It refuses an empty segment, `.`, `..`, and any of `/ ? # % \` or control characters, before encoding. - `pathSegments` judges each part on its own, so a branch like `feat/x` or a file path still works. - Used for: `deleteBranch`, `setBranchProtection` (PATCH), `getFile` (per segment), `moduleDirsAt`/`holdsFile`, `combinedStatus`, and every `/repos/<owner>/<name>` through `repoPath`. That last one covers `gitea_open_pulls` (#159), whose names come from the forge's own answer. - `gitea_branch_protection_set` also judges the rule's branch name, so a rule named `..` is never created. - **Commits checked.** `setCommitStatus`, `commitStatuses` and `compare` take only a commit (`isCommit`). - **Checked again at send.** `GiteaClient.send` runs `checkSentPath` on every request: no empty or dot segment, encoded or not, no fragment, and only the allow-listed query keys. It covers any path built elsewhere too. - **Every interpolated path in the client was grepped.** - What is left: numbers (`index` goes through #159's `indexOf`, comment ids are numbers), `URLSearchParams` built from fixed keys, and the GiteaAdmin class. - GiteaAdmin builds paths with `encodeURIComponent` (or team ids) from the provisioner's manifest settings. It has no tool, so no caller reaches it. - **Merge resolution.** I kept #159's `indexOf` and this PR's `mergePullRequest(…, headCommit)`. Three test fixtures used 6-character shas (`abc123`, `aaa111`, `bbb222`); a commit is 7 to 40 characters, so they are now 7. Test: `no tool's caller text becomes another path or another user`. These 13 cases are refused, with only a read of the repository's own rules reaching the fake forge: - `gitea_delete_branch` with `..`, `feat/../..`, `x?sudo=` and `%2e%2e` - `gitea_get_file` with `../../../../version` and `README.md?sudo=` - `gitea_commit_status` with `abc1234?sudo=` and `../../../admin/users` - `gitea_commit_statuses` with `..` and `main?sudo=` - `gitea_branch_protection_set` with `..` and `main?sudo=` - `gitea_contains` with `..` A branch `feat/x` and a file in a directory still go through. On the previous head the test fails at the first case: `gitea_delete_branch("..")` reached the forge. 50 pass, 0 fail.
mesh-admin merged commit a082615b8c into main 2026-10-09 11:49:43 +00:00
mesh-admin deleted branch fix/the-forge-tools-never-weaken-protection 2026-10-09 11:49:43 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#153