Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+58 -5
View File
@@ -44,7 +44,7 @@ base64-encoded. The Go bundle `cmd/forge` serves the commit, and the reads an ag
| tool | | what |
|---|---|---|
| `gitea_commit` | a | one commit on a branch of its own, created from `base` (default the default branch) when absent, carrying either `files` (each `{path, content \| content_base64 \| delete: true}`) or `patch` (a unified diff, as `git diff` writes it); answers the commit and its parent |
| `gitea_commit` | a | one commit on a branch of its own, created from `base` (default the default branch) when absent, carrying either `files` (each `{path, content \| content_base64 \| delete: true}`) `patch` (a unified diff, as `git diff` writes it), or `patch_file` with `patch_sha256` (a patch too large to type, see below); answers the commit and its parent |
| `gitea_commits` | r | a ref's history, newest first, optionally only commits touching a path: sha, author, date, subject |
| `gitea_compare` | r | what head holds that base does not: its commits and the files they touch |
| `gitea_pull_reviews` | r | a pull request's reviews — state, the commit reviewed and whether it is the head now, stale, dismissed — with their line comments |
@@ -52,13 +52,28 @@ base64-encoded. The Go bundle `cmd/forge` serves the commit, and the reads an ag
| `gitea_search_code` | r | the lines of a ref's text files matching a literal string or a regular expression, optionally under a path |
**Where a commit may go** is the rule `gitea_api` already holds for a file written (novox/hq ADR 0266):
never the repository's default branch, never a branch a protection rule names. A rule covers a branch as the
never the repository's default branch, never a branch a protection rule names. The tools' account may not read the
rules themselves (the forge keeps them for a repository's administrator, novox/hq ADR 0365): for it an existing branch
is judged by the protection the forge says it applies there, and a branch not made yet by the forge itself, which
refuses the commit that would make a branch a rule guards. A rule covers a branch as the
forge itself decides it (Gitea 1.27, novox/hq issue 539): a plain name covers that branch in any case; a glob
(`*`, `**`, `?`, `[abc]`, `[!abc]`, `{a,b}`, `\` escaping) covers case-sensitively with `/` as separator; a glob
the forge cannot compile covers its own text. `cmd/forge/rules.go` and `guard.ts` hold this one rule. The rules are read before anything is sent; rules that cannot be read refuse the
commit. Nothing about merging changes: a merge still needs an independent review at the head and both
required checks green, through `gitea_merge_pull_request`.
**A patch too large to type** (novox/hq issue 852: a feature's diff is megabytes) goes as `patch_file`, with
`patch_sha256`. The caller writes the patch into the module's spool directory `forge-patches`
(`/var/lib/mesh/forge-patches`, mode 0733 and owned by the machine's account, which the tools run as: anyone writes,
nobody lists; the bundle is given its path as `MESH_GITEA_PATCH_DIR`) and names it: `patch_file` is the file's NAME,
32 to 64 characters of `0-9a-f` that the caller chooses at random, never a path. The tool then reads that one file,
applies it exactly as it applies `patch` (same size limit, same answer) and removes it once it has found it, whether
the commit was made or not. A file refused before that (a bad name, a missing hash or spool directory, a name with no
file) stays in the spool. It refuses, sending nothing, a name of another shape, a file that is missing, empty, over
4 MiB, with more than one hard link or not a regular file (a symlink is never followed), and a file whose SHA-256 is not `patch_sha256`: the directory has no sticky bit
(the manifest allows none), so another writer could replace a file, and the hash is how the caller knows the tool read
the bytes it wrote. It reads nothing outside the directory.
**How the commit is made.** `files` become one call to the forge's multi-file contents endpoint: a path the
ref holds is updated, one it does not is created, and an update or delete carries the blob read just
before, so a file someone changed meanwhile is refused by the forge rather than overwritten. The blobs are read at
@@ -66,7 +81,7 @@ the commit the branch (or, for a new branch, its base) was read at, never at its
forge's diff endpoint, which applies it; for a small change to a large file this is far shorter than the
file. Neither endpoint takes a commit to build on, only a branch (Gitea 1.27), so the parent answered is the
one the forge says it made the commit on; when the branch moved between the read and the commit, the answer
says so. One commit carries at most 200 files, 4 MiB per file and 16 MiB together, or a patch of 4 MiB. Either way the commit is the forge's own, made as the forge's admin account, so its sha is not the
says so. One commit carries at most 200 files, 4 MiB per file and 16 MiB together, or a patch of 4 MiB. Either way the commit is the forge's own, made as the tools' account, so its sha is not the
local commit's: the merge check judges the head on the forge. After the write the branch is read back, and a
branch not at the commit the forge named is a failure, never a success.
@@ -75,8 +90,10 @@ is read once and searched in memory; a binary file is passed over, a file over 4
`skipped`, and at most `max` matches (default 100) are answered, with `truncated` when more were found. A
search that reaches the call's deadline stops there and answers what it found, with `stopped_at_deadline`.
It reaches the forge through the same HTTP client as the npm-registry bundle (`internal/forgehttp`), and one
tool call takes at most 50 seconds.
It reaches the forge through the same HTTP client as the npm-registry bundle (`internal/forgehttp`), as the
tools' own account (novox/hq ADR 0365 §2, issue 469): the token the TypeScript bundle mints for `mesh-tools` and
keeps (`MESH_GITEA_TOKEN_FILE`), sent only when its scopes line names that account, never the admin login. One tool
call takes at most 50 seconds.
`go test ./cmd/forge` runs it against a fake forge: the files of one commit created, updated and deleted on
a new branch from its base; an existing branch committed on; the default branch, a protected branch and
unreadable rules refused before anything is sent; a patch sent to the forge; a branch read back at another
@@ -85,6 +102,42 @@ changed under the commit refused; a commit over its size limits refused; protect
forge matches them; a code search stopped at its deadline; every malformed ask refused; and each read
summarised.
## Each agent's read credential (novox/hq ADR 0365 §1, §3–§5, issue 737)
An agent clones and fetches the repositories it may read as a forge account of its own, `agent-<node>`. The
module offers the provision `gitea-read`, whose credential its software issues (ADR 0305) one per holder (issue
749): the Go bundle `cmd/agent-token` serves `gitea_agent_token`, which the vault alone asks (ADR 0349 clause 10;
any other caller is refused and nothing is minted). For each holder the vault names that is the claude-code module
on a machine, it
- makes the account `agent-<node>` with the administrator's login, when there is none: no administrator, a random
password sent once and kept nowhere, and **in no team** — it reads public repositories until the operator adds it
to a team that reads more (§3). An account found made an administrator, inactive or barred from logging in is
made a plain account again;
- deletes the account's token by the name `mesh-agent-read` (by its id, every one of them) and mints one whose one
scope is `read:repository`: whatever team it is put in, the forge refuses it a push, a comment and any token or
administration of its own (§5). Every ask mints anew; that is its rotation;
- answers git's credential-store lines for it — the forge's public origin, read from `origins` (the route's name),
and on the forge's own machine its address there too — sealed to the key the vault made for the call. A holder of
another module is not answered and said under `skipped`.
The vault keeps each as `provision/<forge's node>/gitea-read/<node>/claude-code` and hands it to that holder alone;
the claude-code module places it in the agent's home (ADR 0365 §4). The token is never logged, answered in the clear
or put in an error. `go test ./cmd/agent-token` runs it against a fake forge.
## The agents' read teams (novox/hq ADR 0365 §3, issue 758)
An agent's account reads more than public repositories only through a team, per repository, and only to read. Which
teams exist and who is in them is the operator's, and it is one trusted config value, `agent-read-teams`:
`{"<team>": {"repos": ["<owner>/<repo>", ...], "members": ["<node>", ...]}}`, a member `<node>` being the account
`agent-<node>`. The module renders it into `agent-teams.json` and runs `cmd/agent-teams` when it starts and
whenever that file changes. For each team it makes `agent-read-<team>` in the organisation of the first repository's
owner, with permission read and no other right, its repositories exactly the listed ones and its members exactly the
listed accounts (an account that does not exist yet is skipped and said). A team named `agent-read-*` that has left
the value is emptied, never deleted. A team without that prefix is never touched. There are no verbs: a change is a
change of the value. It says each change in one line on stdout, and never a secret. `go test ./cmd/agent-teams`
runs it against a fake forge.
## The package registry's retention (novox/hq ADR 0251 §4, to-be 51)
Every publish adds a version to the registry, and until this nothing removed one. The Go bundle