// The account that sets commit statuses and merges writes every repository that builds a registered module, and no other // (the operator, 2026-10-26: "yes the mesh should get write access", on the repositories the mesh delivers; novox/hq issue // 1945). car-hunter #2 (a repository of a user, not of the organisation novox) was approved, and mesh-delivery's first // act, its commit status, was refused 403: the tools' account was only a collaborator that reads there (readall.ts). // // **Which account.** The tools' account, `mesh-tools`: the gitea module's own tools set every commit status and merge // (gitea_commit_status, gitea_merge_pull_request) with its token. The build modules' `nox-builder` and the machines' // `nox-agent` only fetch, and keep read (readall.ts). // // **Which repositories: the indexer's word, read over the bus.** mesh-delivery-indexer keeps every registered module in its // state `modules` (mesh-sdk go/delivery ModulesBucket), each value naming the repository it builds from. This module reads // that state (its manifest's `reads`) with a watch: the watch hands the latest value of every key when it starts, and then // every change, so a repository is granted at the module's start and when a module of it is first registered. Nothing // polls. A repository of the organisation needs nothing: the organisation's team writes it (tools-account.ts). Every other // repository of a user (the operator's dotfiles, say) is left as it is: read only. // // **Every grant is read back** from the forge (its permission for the account), and said on the bus as // `repo.writable` {owner, repo, account}: mesh-delivery-gate, which does not ask the operator's Approve of a pull request // whose merge the forge would refuse, reads that repository again on it. // // Not taken away: a repository whose last module goes from the indexer keeps its collaborator until a hand removes it; // gitea_unwritable_repos and gitea_grant_write say the delivered list as it is now. import type { ForgeRepository } from "./client.js"; /** The indexer's state this module reads: one key per registered module, its value naming the repository. */ export const INDEXER_MODULES = "mesh-delivery-indexer.modules"; /** The event said once an account writes a repository it could not write before. */ export const WRITABLE_EVENT = "repo.writable"; /** What the module asks of the forge for this, with the site administrator's login (GiteaAdmin, client.ts). */ export interface WriteForge { everyRepository(): Promise; repository(owner: string, repo: string): Promise; repositoryPermission(owner: string, repo: string, user: string): Promise; addWriteCollaborator(owner: string, repo: string, user: string): Promise; } /** One change of the indexer's modules state, as the SDK's state watch hands it. */ export interface ModulesChange { key: string; op: "put" | "delete"; value?: unknown; revision?: number; current?: boolean; } export interface Written { account: string; /** The repositories that build a registered module, as the indexer says now. */ delivered: string[]; /** Made writable now, each read back from the forge. */ granted: string[]; /** Written already (the organisation's team, or a collaborator that writes). */ already: string[]; /** The forge refused, in its own words; or the repository is the organisation's, whose team should write it. */ refused: { repository: string; why: string }[]; /** Repositories the account cannot write that are left so, and why: they build no registered module. */ left_read_only: { repository: string; permission: string; why: string }[]; } const WRITES = new Set(["write", "admin", "owner"]); const said = (err: unknown): string => (err instanceof Error ? err.message : String(err)); function split(full: string): [string, string] { const [owner, name, ...rest] = String(full ?? "").split("/"); if (!owner || !name || rest.length) throw new Error(`"${full}" is not a repository: say owner/name`); return [owner, name]; } export class DeliveredWrites { /** Module to the repository it builds from, as the indexer's state says. */ readonly modules = new Map(); /** Whether the watch has handed the indexer's state at all: until then, which repositories are delivered is not known. */ heard = false; /** Repositories seen written by the account in this process: a module registered again asks nothing more. */ readonly writable = new Set(); constructor(private readonly forge: WriteForge, readonly account: string, readonly org: string, private readonly log: (line: string) => void = (l) => console.log(`[gitea] ${l}`), private readonly emit: (type: string, body: unknown) => Promise = async () => {}) {} /** The repositories that build a registered module, sorted. */ delivered(): string[] { return [...new Set(this.modules.values())].sort(); } /** One change of the indexer's modules: a repository newly delivered is made writable now. Never throws. */ async take(change: ModulesChange): Promise { this.heard = true; if (change.op === "delete") { this.modules.delete(change.key); return; } const repository = String((change.value as { repository?: unknown } | undefined)?.repository ?? "").trim(); try { split(repository); } catch { this.log(`${INDEXER_MODULES} ${change.key} names no repository (${JSON.stringify(repository)}): no write is granted for it`); return; } this.modules.set(change.key, repository); if (this.writable.has(repository)) return; try { const r = await this.grant([repository]); for (const g of r.granted) this.log(`${this.account} was given write on ${g}, which builds the registered module ${change.key}`); for (const x of r.refused) this.log(`${this.account} could not be given write on ${x.repository}, which builds ${change.key} — ${x.why}`); } catch (err) { this.log(`whether ${this.account} writes ${repository} (module ${change.key}) could not be settled — ${said(err)}`); } } /** Grant write on every delivered repository the account cannot write (or on those named, each of which must be * delivered); with none named, also say which repositories it cannot write are left so, and why. */ async grant(only?: string[]): Promise { if (!this.heard) { throw new Error(`which repositories build a registered module is not known yet: ${INDEXER_MODULES} has not been read ` + "(the watch is not granted or not answered); nothing is granted"); } const delivered = this.delivered(); const result: Written = { account: this.account, delivered, granted: [], already: [], refused: [], left_read_only: [] }; const named = only && only.length ? only.map((x) => String(x).trim()) : null; const targets: string[] = []; for (const full of named ?? delivered) { split(full); if (delivered.includes(full)) targets.push(full); else result.left_read_only.push({ repository: full, permission: "unchanged", why: "builds no registered module (the indexer names none from it), so the account is given no write there" }); } for (const full of targets) { const [owner, name] = split(full); let permission: string; try { if (!(await this.forge.repository(owner, name))) { result.refused.push({ repository: full, why: "the forge holds no repository by that name" }); continue; } permission = await this.forge.repositoryPermission(owner, name, this.account); } catch (err) { result.refused.push({ repository: full, why: `its permission could not be read — ${said(err)}` }); continue; } if (WRITES.has(permission)) { this.writable.add(full); result.already.push(full); continue; } if (owner === this.org) { // The organisation's repositories are written through its team (tools-account.ts), never a collaborator. result.refused.push({ repository: full, why: `a repository of ${this.org}, which ${this.account} writes through the ` + `team ${this.org}/${this.account}; the forge gives it "${permission}", so the team is not as it should be (it is ` + "set again at the next mint of the tools' token)" }); continue; } try { await this.forge.addWriteCollaborator(owner, name, this.account); } catch (err) { result.refused.push({ repository: full, why: said(err) }); continue; } let after: string; try { after = await this.forge.repositoryPermission(owner, name, this.account); } catch (err) { result.refused.push({ repository: full, why: `granted, but its permission could not be read back — ${said(err)}` }); continue; } if (!WRITES.has(after)) { result.refused.push({ repository: full, why: `the grant was answered, but the forge still gives ${this.account} "${after}"` }); continue; } this.writable.add(full); result.granted.push(full); await this.emit(WRITABLE_EVENT, { owner, repo: name, account: this.account }).catch((err) => this.log(`${this.account} writes ${full} now, and that could not be said on the bus — ${said(err)}`)); } if (!named) { for (const repo of await this.forge.everyRepository()) { if (repo.owner === this.org || delivered.includes(repo.full_name)) continue; let permission: string; try { permission = await this.forge.repositoryPermission(repo.owner, repo.name, this.account); } catch (err) { permission = `unknown — ${said(err)}`; } if (WRITES.has(permission)) continue; result.left_read_only.push({ repository: repo.full_name, permission, why: "builds no registered module (the indexer names none from it), so the account is given no write there" }); } } return result; } /** Every delivered repository the account cannot write, as the forge says now, and why. */ async unwritable(): Promise<{ account: string; delivered: number; unwritable: { repository: string; permission: string; why: string }[] }> { if (!this.heard) throw new Error(`which repositories build a registered module is not known yet: ${INDEXER_MODULES} has not been read`); const delivered = this.delivered(); const out: { repository: string; permission: string; why: string }[] = []; for (const full of delivered) { const [owner, name] = split(full); let permission: string; try { permission = await this.forge.repositoryPermission(owner, name, this.account); } catch (err) { out.push({ repository: full, permission: "unknown", why: `the forge did not answer ${this.account}'s permission on it — ${said(err)}` }); continue; } if (WRITES.has(permission)) continue; const modules = [...this.modules].filter(([, r]) => r === full).map(([m]) => m).sort(); out.push({ repository: full, permission, why: `builds ${modules.join(", ")}, and the forge gives ${this.account} ` + `"${permission}" on it: its commit status and its merge are refused until gitea_grant_write grants it` }); } return { account: this.account, delivered: delivered.length, unwritable: out }; } } type Tool = { name: string; description: string; input: Record; run: (args: Readonly>) => Promise }; /** The two tools: which delivered repositories the merging account cannot write, and the grant now. */ export function writeTools(writes: () => DeliveredWrites): Tool[] { return [ { name: "gitea_unwritable_repos", description: "Every repository that builds a registered module (the indexer's modules, read over the bus) on which " + "the mesh's merging forge account (mesh-tools, which sets commit statuses and merges) cannot write, with the " + "permission the forge gives it there and why: such a pull request can be checked but never merged, and the " + "delivery gate does not ask its Approve. Empty when it writes every one. Read with the site administrator's login.", input: {}, run: async () => writes().unwritable(), }, { name: "gitea_grant_write", description: "Give the mesh's merging forge account (mesh-tools) write, as a collaborator, on every repository that " + "builds a registered module and that it cannot write now, or on the repositories named (each must build one). " + "The module does this itself at its start and when a module of a new repository is registered; this is the same " + "act by hand, such as when a merge or a commit status was refused 403. A repository of the organisation novox " + "is written through its team and needs nothing. Answers what was granted (each read back from the forge), what " + "was written already, what the forge refused in its own words, and which other repositories it left read-only " + "and why (they build no registered module). Needs the site administrator's login, used for this grant alone.", input: { repositories: { type: "array", items: { type: "string" }, description: "owner/name of each repository; empty or absent, every delivered one" }, }, run: async (args) => writes().grant(Array.isArray(args.repositories) ? args.repositories.map(String) : undefined), }, ]; }