The mesh's forge accounts all read every repository, and a build's fetch says the forge's own words
mesh/delivery merged: source.main moved to novox/mesh-catalog@61563abf2f93
mesh/delivery-group group fix/fetch-reads-all merged: every member is merged
mesh/check checking 071c6885d923 on 8393e2962193

mesh-catalog #495 gave read on every repository to the tools' account alone. The build modules
fetch over ssh as nox-builder (the forge-account provider's builders account, ADR 0466 §3), so
ace's and g14's fetch of jschoubben/car-hunter and jschoubben/snake still failed; the machines'
agents (nox-agent, ADR 0410 §1) could not read them either.

- gitea: readall.ts grants every account of one list, meshAccounts in tools-account.ts (mesh-tools,
  nox-builder, nox-agent; a Go test holds the two Go names to internal/forgeaccount's constants),
  on the same start pass, forge events and tools. gitea_unreadable_repos answers per account and
  names the accounts the forge does not hold yet; gitea_grant_read takes accounts as well as
  repositories. An absent account is asked nothing; the start pass runs again ten minutes apart
  until every account was there once.
- mesh-build-go, mesh-build-node, mesh-build-image: pin mesh-sdk ff81bdf8 (fix/fetch-reads-all),
  whose forgekey.Said keeps git's words past the bare "error:" line: the mirror's line said only
  "git -C: error:". Their own git reasons (ls-files, ls-remote, rev-parse, repack) go through it too.
This commit is contained in:
2026-10-26 15:27:38 +01:00
parent ceb3aac9b3
commit 071c6885d9
19 changed files with 293 additions and 141 deletions
+15 -9
View File
@@ -275,12 +275,17 @@ reported no key gets no entry, and its client says it waits. Keys titled `mesh:`
a key added by hand never is. Every consumer is made again every ten minutes and at once when a key changes.
`go test ./cmd/forge-account-provider ./internal/forgeaccount` runs it against a fake forge.
## The tools' account reads every repository (the operator, 2026-10-26)
## The mesh's accounts read every repository (the operator, 2026-10-26)
The tools act as the plain account `mesh-tools` (ADR 0365 §2, issue 469), which writes to the organisation `novox`
through its team. Any other repository it saw only when public, so a private repository of a user answered every tool
404 and was missing from every list. `readall.ts` makes the account a collaborator that **reads only** on each
repository it cannot read: one repository at a time, no organisation membership, the least the forge offers.
through its team; the build modules fetch over ssh as `nox-builder` (ADR 0466 §3), in a team that reads the
organisation's code; the machines' agents use `nox-agent` (ADR 0410 §1). Any other repository each saw only when public,
so a private repository of a user answered every tool 404 and every build's fetch "not authorized to read". `readall.ts`
makes each of these accounts (one list, `meshAccounts` in `tools-account.ts`, whose two Go names
`internal/forgeaccount`'s test holds to its own constants) a collaborator that **reads only** on each repository it
cannot read: one repository at a time, no organisation membership, the least the forge offers. An account the forge
does not hold yet (the forge-account provider makes `nox-builder` and `nox-agent` when their first consumer asks) is
named and asked nothing; the start's pass is run again ten minutes apart until every account was there once.
- **When:** one pass over every repository of every user and organisation at the start (a forge not answering yet is
asked again a minute apart until one pass ran), and on the forge's system webhook: `forge.repository.created` and
@@ -292,11 +297,12 @@ repository it cannot read: one repository at a time, no organisation membership,
the forge will not answer is named as refused (or, in the listing, with permission `unknown`) and the pass goes on.
- **With what:** the site administrator's login (`MESH_GITEA_ADMIN_PASSWORD_FILE`), because adding a collaborator asks a
repository's administration, which on another user's repository only the site administrator holds. It is used for
four calls alone: list every repository, read one, read the account's permission on one, add the account as a read
collaborator. The tools' own calls keep their token.
- **Tools:** `gitea_unreadable_repos` (every repository the account cannot read, the permission the forge gives it
there and why) and `gitea_grant_read` (the same grant by hand, now, on every repository or on those named; each grant
read back from the forge, a refusal answered in the forge's own words).
five calls alone: list every repository, read one, whether an account exists, read an account's permission on one,
add an account as a read collaborator. The tools' own calls keep their token.
- **Tools:** `gitea_unreadable_repos` (per account, every repository it cannot read, the permission the forge gives
it there and why, a count per account and the accounts the forge does not hold) and `gitea_grant_read` (the same
grant by hand, now, on every repository or on those named, for every account or those named; each grant read back
from the forge, a refusal answered in the forge's own words).
## Files it does not know
+9
View File
@@ -1013,6 +1013,15 @@ export class GiteaAdmin {
return forgeRepository(res.body);
}
/** Whether the forge holds an account of this name. */
async accountExists(user: string): Promise<boolean> {
const path = `/users/${encodeURIComponent(user)}`;
const res = await this.request(path);
if (res.status === 404) return false;
if (res.status !== 200) GiteaAdmin.fail(path, res);
return true;
}
/** What the forge itself says an account may do on a repository: none, read, write, admin or owner. */
async repositoryPermission(owner: string, repo: string, user: string): Promise<string> {
const path = `${repoPath(owner, repo)}/collaborators/${encodeURIComponent(user)}/permission`;
+13 -7
View File
@@ -61,7 +61,7 @@ import { keepSiblings, noteComposed, noteVerdict, readSiblings, siblingsFile, si
import { bindInBackground } from "./bind.js";
import { announcementOf, keepTook, lateVerdictComment, outOfDateWords, sameCommit, tookFile, weighTrunks } from "./trunk.js";
import { ReadAll, readAllTools } from "./readall.js";
import { TOOLS_ACCOUNT } from "./tools-account.js";
import { TOOLS_ACCOUNT, meshAccounts } from "./tools-account.js";
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
// nothing to watch; log and stay quiet rather than crash the runtime. With one, the first poll mints
@@ -502,11 +502,12 @@ if (!gitea) pace.unheard("the forge is not watched: no way to a token", false);
// Said with `emit`, not the SDK's `keeps`, which the toolchain's SDK does not have yet: switch to `keeps` once that SDK is 0.1.15 or later (issue 1852).
await emit("states", statesAnnouncement(process.env.MESH_MODULE || "gitea", process.env.MESH_NODE));
// **The tools' account reads every repository** (readall.ts; the operator, 2026-10-26): granted with the site
// **The mesh's accounts read every repository** (readall.ts; the operator, 2026-10-26): the tools', the build modules'
// and the machines' agents' (meshAccounts, tools-account.ts), granted with the site
// administrator's login, which the module holds for setting the forge up and uses here for that grant alone. Made on
// first use, so a module whose admin login is missing still serves the rest and the tools say why.
let readAllOnce: ReadAll | null = null;
const readAll = (): ReadAll => (readAllOnce ??= new ReadAll(GiteaAdmin.fromEnv(), process.env.MESH_GITEA_TOOLS_USER || TOOLS_ACCOUNT));
const readAll = (): ReadAll => (readAllOnce ??= new ReadAll(GiteaAdmin.fromEnv(), meshAccounts(process.env.MESH_GITEA_TOOLS_USER || TOOLS_ACCOUNT)));
// gitea_events is served whether or not the forge is watched: it says so (novox/hq issue 729).
registerModuleTools("gitea", () => [eventsTool(pace), ...readAllTools(readAll), ...(gitea ? [commitStatusTool({
@@ -554,7 +555,7 @@ async function listenForForge(client: GiteaClient): Promise<boolean> {
act: async (event) => {
// A repository made, or one not yet seen readable, is granted read before anything acts on it (readall.ts).
await Promise.resolve().then(() => readAll().onEvent(event)).catch((err) =>
console.error(`[gitea] whether ${TOOLS_ACCOUNT} reads the repository of ${event.name} could not be settled — ${err instanceof Error ? err.message : err}`));
console.error(`[gitea] whether the mesh's accounts read the repository of ${event.name} could not be settled — ${err instanceof Error ? err.message : err}`));
await actOn(client, event);
} });
await new Promise<void>((resolve, reject) => {
@@ -609,15 +610,20 @@ if (gitea) {
void listenForForge(client);
// The start's one pass over every repository (readall.ts), not awaited for the same reason. A forge not answering yet
// is asked again, a minute apart, until one pass has run; after that only the forge's events grant.
const readAllAtStart = (): void => void Promise.resolve().then(() => readAll().atStart()).catch((err) => {
console.error(`[gitea] whether ${TOOLS_ACCOUNT} reads every repository could not be settled; asked again in a minute — ${err instanceof Error ? err.message : err}`);
// An account of the list the forge does not hold yet (the forge-account provider makes nox-builder and nox-agent when
// their first consumer asks, and the forge sends no event for a user made) is looked for again ten minutes apart
// until every account was granted once; the line of each pass names it.
const readAllAtStart = (): void => void Promise.resolve().then(() => readAll().atStart()).then((r) => {
if (r.absent.length) setTimeout(readAllAtStart, 600_000);
}, (err) => {
console.error(`[gitea] whether the mesh's accounts read every repository could not be settled; asked again in a minute — ${err instanceof Error ? err.message : err}`);
setTimeout(readAllAtStart, 60_000);
});
try {
readAll();
readAllAtStart();
} catch (err) {
console.error(`[gitea] ${TOOLS_ACCOUNT} is not given read on every repository: no site administrator's login — ${err instanceof Error ? err.message : err}`);
console.error(`[gitea] the mesh's accounts are not given read on every repository: no site administrator's login — ${err instanceof Error ? err.message : err}`);
}
// Main takes no push and only the tools' account merges, in every repository, kept and not only when a token is
// minted: at the start and every ten minutes after, so a repository made later gets it too (issue 948, ADR 0398 §2).
@@ -1,6 +1,10 @@
package forgeaccount
import "testing"
import (
"os"
"regexp"
"testing"
)
func TestForgeUserOfAMachineAccountIsTheOneAgentUser(t *testing.T) {
if got := ForgeUser("g14", "machine"); got != "nox-agent" {
@@ -15,6 +19,18 @@ func TestForgeUserOfAMachineAccountIsTheOneAgentUser(t *testing.T) {
if got := NodeAccount("g14"); got != "agent-g14" {
t.Fatalf("the token tool's per-node account = %q", got)
}
// The accounts that read every repository (readall.ts, meshAccounts in tools-account.ts) name these two users: the
// list is held to the Go constants here, so the two cannot drift apart (the operator, 2026-10-26).
ts, err := os.ReadFile("../../tools-account.ts")
if err != nil {
t.Fatal(err)
}
for name, want := range map[string]string{"BUILDER_ACCOUNT": BuilderUser, "AGENT_ACCOUNT": AgentUser} {
m := regexp.MustCompile(`export const ` + name + ` = "([^"]+)";`).FindSubmatch(ts)
if m == nil || string(m[1]) != want {
t.Errorf("tools-account.ts %s = %q, want %q (internal/forgeaccount)", name, m, want)
}
}
}
func TestRemoveKeysTitledTakesOnlyTheMeshKeysOfThatTitle(t *testing.T) {
+144 -91
View File
@@ -1,5 +1,12 @@
// The tools' account reads every repository on the forge (the operator, 2026-10-26: "the mesh's forge account should
// get read access to all repositories").
// The mesh's own forge accounts read every repository on the forge (the operator, 2026-10-26: "the mesh's forge account
// should get read access to all repositories"; and "why wouldn't the forge account get access to all repo's?").
//
// **Every account the mesh's parts use, from one list** (MESH_ACCOUNTS, tools-account.ts): the tools' `mesh-tools`, the
// build modules' `nox-builder` (their fetch over ssh, ADR 0466 §3) and the machines' `nox-agent` (ssh-client's key,
// ADR 0410 §1). The first pass granted the tools' account alone, so the build modules' fetch of jschoubben/car-hunter
// still failed on ace and g14 ("nox-builder ... is not authorized to read"). The two Go names are the ones of
// internal/forgeaccount, whose test holds the list to them. An account the forge does not hold yet (its consumer never
// asked) is said and left out, never a refusal per repository.
//
// Since issue 469 the tools act as the plain account `mesh-tools`, which writes to the organisation novox's
// repositories through its team (tools-account.ts). Every other repository it saw only when public: a private
@@ -32,12 +39,15 @@ import type { ForgeRepository } from "./client.js";
export interface ReadAllForge {
everyRepository(): Promise<ForgeRepository[]>;
repository(owner: string, repo: string): Promise<ForgeRepository | null>;
/** Whether the forge holds the account at all. */
accountExists(user: string): Promise<boolean>;
repositoryPermission(owner: string, repo: string, user: string): Promise<string>;
addReadCollaborator(owner: string, repo: string, user: string): Promise<void>;
}
/** A repository the account cannot read, and why. */
/** A repository an account cannot read, and why. */
export interface Unreadable {
account: string;
repository: string;
permission: string;
private: boolean;
@@ -46,16 +56,22 @@ export interface Unreadable {
}
export interface Granted {
/** Repositories looked at. */
/** Repositories looked at (for each account). */
checked: number;
/** Made readable now, by a read collaborator. */
granted: string[];
/** The forge refused the grant: each repository and the forge's own words. */
refused: { repository: string; why: string }[];
/** The accounts looked at: those the forge holds. */
accounts: string[];
/** Accounts of the list the forge does not hold yet: nothing is granted to them until it does. */
absent: string[];
/** Made readable now, by a read collaborator: each repository and the account. */
granted: { account: string; repository: string }[];
/** The forge refused the grant: each repository, the account and the forge's own words. */
refused: { account: string; repository: string; why: string }[];
}
const READS = new Set(["read", "write", "admin", "owner"]);
const said = (err: unknown): string => (err instanceof Error ? err.message : String(err));
/** Why the account cannot read a repository, from what the forge says of it. */
export function whyUnreadable(repo: ForgeRepository, account: string): string {
const parts: string[] = [];
@@ -68,31 +84,53 @@ export function whyUnreadable(repo: ForgeRepository, account: string): string {
}
export class ReadAll {
/** Repositories this process has seen the account read, by full name: an event of one of them asks nothing more. */
readonly readable = new Set<string>();
/** Per account, the repositories this process has seen it read, by full name: an event of a repository every account
* was seen reading asks nothing more. */
readonly readable = new Map<string, Set<string>>();
readonly accounts: readonly string[];
constructor(private readonly forge: ReadAllForge, readonly account: string,
private readonly log: (line: string) => void = (l) => console.log(`[gitea] ${l}`)) {}
/** Every repository the account cannot read, and why, as the forge says it now. */
async unreadable(): Promise<{ checked: number; unreadable: Unreadable[] }> {
const repos = await this.forge.everyRepository();
const out: Unreadable[] = [];
for (const repo of repos) {
try {
const u = await this.judge(repo);
if (u) out.push(u);
} catch (err) {
// A permission the forge would not answer is said for that repository; the rest are still looked at.
out.push({ repository: repo.full_name, permission: "unknown", private: repo.private, archived: repo.archived,
why: `the forge did not answer ${this.account}'s permission on it — ${err instanceof Error ? err.message : String(err)}` });
}
}
return { checked: repos.length, unreadable: out };
constructor(private readonly forge: ReadAllForge, accounts: readonly string[] | string,
private readonly log: (line: string) => void = (l) => console.log(`[gitea] ${l}`)) {
this.accounts = [...new Set(typeof accounts === "string" ? [accounts] : accounts)];
if (!this.accounts.length) throw new Error("no forge account to give read: the list is empty");
for (const a of this.accounts) this.readable.set(a, new Set());
}
/** Grant read on every repository the account cannot read (or on those named, `owner/name`). */
async grant(only?: string[]): Promise<Granted> {
/** The accounts of the list (or of those named) the forge holds, and those it does not hold yet. */
private async present(only?: string[]): Promise<{ accounts: string[]; absent: string[] }> {
const wanted = only && only.length ? only : [...this.accounts];
for (const a of wanted) if (!this.accounts.includes(a)) throw new Error(`"${a}" is not one of the mesh's accounts: ${this.accounts.join(", ")}`);
const accounts: string[] = [];
const absent: string[] = [];
for (const a of wanted) ((await this.forge.accountExists(a)) ? accounts : absent).push(a);
return { accounts, absent };
}
/** Every repository each account cannot read, and why, as the forge says it now. */
async unreadable(only?: string[]): Promise<{ checked: number; accounts: { account: string; unreadable: number }[]; absent: string[]; unreadable: Unreadable[] }> {
const { accounts, absent } = await this.present(only);
const repos = await this.forge.everyRepository();
const out: Unreadable[] = [];
for (const account of accounts) {
for (const repo of repos) {
try {
const u = await this.judge(repo, account);
if (u) out.push(u);
} catch (err) {
// A permission the forge would not answer is said for that repository; the rest are still looked at.
out.push({ account, repository: repo.full_name, permission: "unknown", private: repo.private, archived: repo.archived,
why: `the forge did not answer ${account}'s permission on it — ${said(err)}` });
}
}
}
return { checked: repos.length, accounts: accounts.map((account) => ({ account, unreadable: out.filter((u) => u.account === account).length })),
absent, unreadable: out };
}
/** Grant read on every repository an account cannot read (or on those named, `owner/name`), for every account of the
* list the forge holds (or for those named). */
async grant(only?: string[], accountsOnly?: string[]): Promise<Granted> {
const { accounts, absent } = await this.present(accountsOnly);
let repos: ForgeRepository[];
if (only && only.length) {
repos = [];
@@ -104,36 +142,39 @@ export class ReadAll {
repos.push(repo);
}
} else repos = await this.forge.everyRepository();
const result: Granted = { checked: repos.length, granted: [], refused: [] };
for (const repo of repos) {
let u: Unreadable | null;
try {
u = await this.judge(repo);
} catch (err) {
// One repository whose permission cannot be read is a refusal of its own, never the end of the pass.
result.refused.push({ repository: repo.full_name, why: `its permission could not be read — ${err instanceof Error ? err.message : String(err)}` });
continue;
}
if (!u) continue;
try {
await this.forge.addReadCollaborator(repo.owner, repo.name, this.account);
} catch (err) {
result.refused.push({ repository: repo.full_name, why: err instanceof Error ? err.message : String(err) });
continue;
}
// Read back: the forge's own word that the account reads it now, never the grant's answer alone.
let after: string;
try {
after = await this.forge.repositoryPermission(repo.owner, repo.name, this.account);
} catch (err) {
result.refused.push({ repository: repo.full_name, why: `granted, but its permission could not be read back — ${err instanceof Error ? err.message : String(err)}` });
continue;
}
if (READS.has(after)) {
this.readable.add(repo.full_name);
result.granted.push(repo.full_name);
} else {
result.refused.push({ repository: repo.full_name, why: `the grant was answered, but the forge still gives ${this.account} "${after}"` });
const result: Granted = { checked: repos.length, accounts, absent, granted: [], refused: [] };
for (const account of accounts) {
for (const repo of repos) {
const repository = repo.full_name;
let u: Unreadable | null;
try {
u = await this.judge(repo, account);
} catch (err) {
// One repository whose permission cannot be read is a refusal of its own, never the end of the pass.
result.refused.push({ account, repository, why: `its permission could not be read — ${said(err)}` });
continue;
}
if (!u) continue;
try {
await this.forge.addReadCollaborator(repo.owner, repo.name, account);
} catch (err) {
result.refused.push({ account, repository, why: said(err) });
continue;
}
// Read back: the forge's own word that the account reads it now, never the grant's answer alone.
let after: string;
try {
after = await this.forge.repositoryPermission(repo.owner, repo.name, account);
} catch (err) {
result.refused.push({ account, repository, why: `granted, but its permission could not be read back — ${said(err)}` });
continue;
}
if (READS.has(after)) {
this.readable.get(account)!.add(repository);
result.granted.push({ account, repository });
} else {
result.refused.push({ account, repository, why: `the grant was answered, but the forge still gives ${account} "${after}"` });
}
}
}
return result;
@@ -142,69 +183,81 @@ export class ReadAll {
/** The start's one pass: grant what is missing and say it. Answers the result; a failure is the caller's to say. */
async atStart(): Promise<Granted> {
const r = await this.grant();
this.log(`${this.account} reads every repository: ${r.checked} looked at, ${r.granted.length} granted read now` +
`${r.granted.length ? ` (${r.granted.join(", ")})` : ""}${r.refused.length ? `, ${r.refused.length} refused: ` +
r.refused.map((x) => `${x.repository} — ${x.why}`).join("; ") : ""}`);
const per = r.accounts.map((a) => {
const g = r.granted.filter((x) => x.account === a).map((x) => x.repository);
return `${a} ${g.length} granted read now${g.length ? ` (${g.join(", ")})` : ""}`;
});
this.log(`the mesh's accounts read every repository: ${r.checked} looked at; ${per.join("; ")}` +
`${r.absent.length ? `; not on the forge yet, so not granted: ${r.absent.join(", ")}` : ""}` +
`${r.refused.length ? `; ${r.refused.length} refused: ` + r.refused.map((x) => `${x.account} on ${x.repository} — ${x.why}`).join("; ") : ""}`);
return r;
}
/** A forge event: a repository made (or forked), or the first event of one not seen readable, is granted read now. */
/** A forge event: a repository made (or forked), or the first event of one some account was not seen reading, is
* granted read now to every account that cannot read it. */
async onEvent(event: { name: string; body: unknown }): Promise<void> {
const b = (event.body ?? {}) as { owner?: string; repo?: string };
if (!b.owner || !b.repo) return;
const full = `${b.owner}/${b.repo}`;
if (event.name === "forge.repository.deleted") {
this.readable.delete(full);
for (const seen of this.readable.values()) seen.delete(full);
return;
}
const made = event.name === "forge.repository.created" || event.name === "forge.fork";
// Seen readable once, its events ask nothing: so a repository made private after that is not caught here (the
// forge says no change of visibility), only at the next start or by gitea_grant_read.
if (!made && this.readable.has(full)) return;
// Seen readable once by every account, its events ask nothing: so a repository made private after that is not
// caught here (the forge says no change of visibility), only at the next start or by gitea_grant_read.
if (!made && [...this.readable.values()].every((seen) => seen.has(full))) return;
const r = await this.grant([full]);
if (r.granted.length) this.log(`${this.account} was given read on ${full}, on the forge's ${event.name}`);
for (const x of r.refused) this.log(`${this.account} could not be given read on ${x.repository} — ${x.why}`);
for (const x of r.granted) this.log(`${x.account} was given read on ${full}, on the forge's ${event.name}`);
for (const x of r.refused) this.log(`${x.account} could not be given read on ${x.repository} — ${x.why}`);
}
private async judge(repo: ForgeRepository): Promise<Unreadable | null> {
const permission = await this.forge.repositoryPermission(repo.owner, repo.name, this.account);
private async judge(repo: ForgeRepository, account: string): Promise<Unreadable | null> {
const permission = await this.forge.repositoryPermission(repo.owner, repo.name, account);
const seen = this.readable.get(account)!;
if (READS.has(permission)) {
this.readable.add(repo.full_name);
seen.add(repo.full_name);
return null;
}
this.readable.delete(repo.full_name);
return { repository: repo.full_name, permission, private: repo.private, archived: repo.archived, why: whyUnreadable(repo, this.account) };
seen.delete(repo.full_name);
return { account, repository: repo.full_name, permission, private: repo.private, archived: repo.archived, why: whyUnreadable(repo, account) };
}
}
type Tool = { name: string; description: string; input: Record<string, unknown>; run: (args: Readonly<Record<string, unknown>>) => Promise<unknown> };
/** The two tools: what the account cannot read and why, and the grant now. */
const strings = (v: unknown): string[] | undefined => (Array.isArray(v) ? v.map(String) : undefined);
/** The two tools: what each account cannot read and why, and the grant now. */
export function readAllTools(readAll: () => ReadAll): Tool[] {
return [
{
name: "gitea_unreadable_repos",
description: "Every repository on the forge the tools' own account (mesh-tools) cannot read, and why: the " +
"permission the forge gives it there, whether the repository is private or archived, and in words the reason " +
"(a private repository of a user with the account no collaborator, an owner not public). Empty when it reads " +
"every repository. Read with the site administrator's login, which alone sees every repository.",
input: {},
run: async () => readAll().unreadable(),
description: "Every repository on the forge one of the mesh's own forge accounts cannot read, and why, per account: " +
"the tools' (mesh-tools), the build modules' fetch over ssh (nox-builder) and the machines' agents' (nox-agent). " +
"For each, the permission the forge gives it there, whether the repository is private or archived, and in words " +
"the reason (a private repository of a user with the account no collaborator, an owner not public); a count per " +
"account, and the accounts the forge does not hold yet. Empty when every account reads every repository. Read " +
"with the site administrator's login, which alone sees every repository.",
input: {
accounts: { type: "array", items: { type: "string" }, description: "the accounts to look at; empty or absent, every one of the mesh's" },
},
run: async (args) => readAll().unreadable(strings(args.accounts)),
},
{
name: "gitea_grant_read",
description: "Give the tools' own account (mesh-tools) read on every repository it cannot read now, or on the " +
"repositories named: each becomes a collaborator that reads only, the least the forge offers. The module does " +
"this itself at its start and on the forge's event of a repository made; this is the same act by hand. Answers " +
"how many were looked at, which were granted (each read back from the forge) and which the forge refused, in its " +
"own words. Needs the site administrator's login, used for this grant alone.",
description: "Give each of the mesh's own forge accounts (mesh-tools, nox-builder, nox-agent) read on every " +
"repository it cannot read now, or on the repositories named, or for the accounts named: each becomes a " +
"collaborator that reads only, the least the forge offers. The module does this itself at its start and on the " +
"forge's event of a repository made; this is the same act by hand, such as when a build's fetch says its account " +
"is not authorized to read a repository. Answers how many were looked at, which were granted to which account " +
"(each read back from the forge), which the forge refused, in its own words, and the accounts it does not hold " +
"yet. 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 repository" },
accounts: { type: "array", items: { type: "string" }, description: "the accounts to grant; empty or absent, every one of the mesh's" },
},
run: async (args) => {
const named = Array.isArray(args.repositories) ? args.repositories.map(String) : undefined;
return readAll().grant(named);
},
run: async (args) => readAll().grant(strings(args.repositories), strings(args.accounts)),
},
];
}
+48 -17
View File
@@ -16,7 +16,7 @@ import { join } from "node:path";
import { MintedToken } from "../dist/token.js";
import { GiteaClient } from "../dist/client.js";
import { TOOLS_ACCOUNT, TOOLS_TEAM_UNITS } from "../dist/tools-account.js";
import { TOOLS_ACCOUNT, TOOLS_TEAM_UNITS, BUILDER_ACCOUNT, AGENT_ACCOUNT, meshAccounts } from "../dist/tools-account.js";
import { repoCheckFacts } from "../dist/pulls.js";
const ADMIN = "mesh-admin";
@@ -447,12 +447,15 @@ test("a mint with keep-main-rules false (the default) writes no branch protectio
assert.deepEqual(writes, []);
});
// The tools' account reads every repository (readall.ts; the operator, 2026-10-26): jschoubben/car-hunter and
// jschoubben/snake, private repositories of a user, answered the tools 404 while that user's public ones were seen.
// The mesh's accounts read every repository (readall.ts; the operator, 2026-10-26): jschoubben/car-hunter and
// jschoubben/snake, private repositories of a user, answered the tools 404 while that user's public ones were seen; and
// once the tools' account read them, the build modules' fetch as nox-builder still could not.
test("the start's pass gives the tools' account read, as a collaborator that reads only, on each repository it could not read, and on no other", async () => {
test("the start's pass gives each of the mesh's accounts read, as a collaborator that reads only, on each repository it could not read, and on no other; an account the forge does not hold is said, not refused", async () => {
const env = await environment();
await fresh(env).listRepos(); // the account and its team exist, as after any mint
forge.users.set(BUILDER_ACCOUNT, { admin: false, password: "unused", active: true }); // made by the forge-account provider
forge.users.delete(AGENT_ACCOUNT); // no machine's key asked for it yet
forgeRepos.splice(0);
addRepo("novox/hq", true);
addRepo("jschoubben/txt-game", false);
@@ -463,14 +466,24 @@ test("the start's pass gives the tools' account read, as a collaborator that rea
const { GiteaAdmin } = await import("../dist/client.js");
const { ReadAll, readAllTools } = await import("../dist/readall.js");
const lines: string[] = [];
const readAll = new ReadAll(GiteaAdmin.fromEnv(env), TOOLS_ACCOUNT, (l: string) => lines.push(l));
const readAll = new ReadAll(GiteaAdmin.fromEnv(env), meshAccounts(), (l: string) => lines.push(l));
assert.deepEqual(readAll.accounts, [TOOLS_ACCOUNT, BUILDER_ACCOUNT, AGENT_ACCOUNT]);
const [unreadableTool, grantTool] = readAllTools(() => readAll);
// What the tool says before: the four the account cannot read, each with why.
// What the tool says before, per account: the four the tools' account cannot read, each with why; the builders'
// account, in no write team here, also not novox/hq; nox-agent not on the forge.
const before = await unreadableTool.run({}) as any;
assert.equal(before.checked, 6);
assert.deepEqual(before.unreadable.map((u: any) => u.repository).sort(),
["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]);
const of = (account: string) => before.unreadable.filter((u: any) => u.account === account).map((u: any) => u.repository).sort();
assert.deepEqual(of(TOOLS_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]);
assert.deepEqual(of(BUILDER_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "novox/hq", "shy/open"]);
assert.deepEqual(before.accounts, [{ account: TOOLS_ACCOUNT, unreadable: 4 }, { account: BUILDER_ACCOUNT, unreadable: 5 }]);
assert.deepEqual(before.absent, [AGENT_ACCOUNT]);
assert.match(before.unreadable.find((u: any) => u.account === BUILDER_ACCOUNT && u.repository === "jschoubben/car-hunter").why,
/a private repository of jschoubben, on which nox-builder is no collaborator/);
const builderOnly = await unreadableTool.run({ accounts: [BUILDER_ACCOUNT] }) as any;
assert.deepEqual(builderOnly.accounts, [{ account: BUILDER_ACCOUNT, unreadable: 5 }]);
await assert.rejects(unreadableTool.run({ accounts: ["someone"] }), /not one of the mesh's accounts/);
assert.match(before.unreadable.find((u: any) => u.repository === "jschoubben/car-hunter").why,
/a private repository of jschoubben, on which mesh-tools is no collaborator/);
assert.match(before.unreadable.find((u: any) => u.repository === "shy/open").why, /visibility is limited/);
@@ -484,13 +497,27 @@ test("the start's pass gives the tools' account read, as a collaborator that rea
const from = forge.calls.length;
const r = await readAll.atStart();
assert.deepEqual(r.granted.sort(), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]);
const grantedTo = (account: string) => r.granted.filter((x: any) => x.account === account).map((x: any) => x.repository).sort();
assert.deepEqual(grantedTo(TOOLS_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]);
assert.deepEqual(grantedTo(BUILDER_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "novox/hq", "shy/open"]);
assert.deepEqual(r.refused, []);
assert.deepEqual(r.absent, [AGENT_ACCOUNT]);
const puts = forge.calls.slice(from).filter((c) => c.startsWith("PUT "));
assert.equal(puts.length, 4, "the repositories it read already were left alone");
assert.equal(puts.length, 9, "the repositories each read already were left alone, and the absent account was asked nothing");
assert.ok(!puts.some((c) => c.includes(AGENT_ACCOUNT)));
assert.ok(puts.every((c) => c.endsWith(` as ${ADMIN}`)), "only the administrator's login grants");
for (const repo of forgeRepos.filter((x) => x.collaborators.size)) assert.equal(repo.collaborators.get(TOOLS_ACCOUNT), "read", `${repo.full} reads only`);
assert.match(lines.join("\n"), /6 looked at, 4 granted read now/);
for (const repo of forgeRepos.filter((x) => x.collaborators.size)) {
for (const [account, permission] of repo.collaborators) assert.equal(permission, "read", `${account} on ${repo.full} reads only`);
}
assert.equal(forgeRepos.find((x) => x.full === "jschoubben/car-hunter")!.collaborators.get(BUILDER_ACCOUNT), "read");
assert.match(lines.join("\n"), /6 looked at; mesh-tools 4 granted read now .*; nox-builder 5 granted read now .*; not on the forge yet, so not granted: nox-agent/);
// The account made later is granted by hand (or at the next pass), its rows like any other.
forge.users.set(AGENT_ACCOUNT, { admin: false, password: "unused", active: true });
const agent = await grantTool.run({ accounts: [AGENT_ACCOUNT], repositories: ["jschoubben/car-hunter"] }) as any;
assert.deepEqual(agent.granted, [{ account: AGENT_ACCOUNT, repository: "jschoubben/car-hunter" }]);
assert.deepEqual(agent.absent, []);
forge.users.delete(AGENT_ACCOUNT);
// After: the account's own token sees every repository, and the tool names none.
assert.equal((await fresh(env).listAllRepos()).length, 6);
@@ -509,7 +536,8 @@ test("on the forge's events: a repository made is granted read at once, a known
const { GiteaAdmin } = await import("../dist/client.js");
const { ReadAll, readAllTools } = await import("../dist/readall.js");
const lines: string[] = [];
const readAll = new ReadAll(GiteaAdmin.fromEnv(env), TOOLS_ACCOUNT, (l: string) => lines.push(l));
forge.users.set(BUILDER_ACCOUNT, { admin: false, password: "unused", active: true });
const readAll = new ReadAll(GiteaAdmin.fromEnv(env), [TOOLS_ACCOUNT, BUILDER_ACCOUNT], (l: string) => lines.push(l));
await readAll.atStart();
const cases: { name: string; event: string; repo: string; make?: boolean; refuse?: boolean; unanswered?: boolean; asks: boolean; reads: string }[] = [
@@ -528,17 +556,20 @@ test("on the forge's events: a repository made is granted read at once, a known
const [owner, repo] = c.repo.split("/");
await readAll.onEvent({ name: c.event, body: { owner, repo } });
assert.equal(forge.calls.length > from, c.asks, `${c.name}: asks the forge`);
assert.equal(forgeRepos.find((x) => x.full === c.repo)!.collaborators.get(TOOLS_ACCOUNT) ?? (c.make ? "none" : "read"), c.reads, c.name);
for (const account of [TOOLS_ACCOUNT, BUILDER_ACCOUNT]) {
assert.equal(forgeRepos.find((x) => x.full === c.repo)!.collaborators.get(account) ?? (c.make ? "none" : "read"), c.reads, `${c.name}: ${account}`);
}
}
assert.match(lines.join("\n"), /could not be given read on jschoubben\/stuck — .*422/);
assert.match(lines.join("\n"), /mesh-tools could not be given read on jschoubben\/stuck — .*422/);
assert.match(lines.join("\n"), /nox-builder was given read on someone\/fork, on the forge's forge.fork/);
const named = await (readAllTools(() => readAll)[1].run({ repositories: ["jschoubben/stuck"] })) as any;
assert.deepEqual(named.refused.map((x: any) => x.repository), ["jschoubben/stuck"]);
assert.deepEqual(named.refused.map((x: any) => `${x.account} ${x.repository}`), [`${TOOLS_ACCOUNT} jschoubben/stuck`, `${BUILDER_ACCOUNT} jschoubben/stuck`]);
await assert.rejects(readAllTools(() => readAll)[1].run({ repositories: ["no-slash"] }), /not a repository/);
assert.match(lines.join("\n"), /could not be given read on jschoubben\/mute — its permission could not be read — .*500/);
// A whole pass goes past the repository it cannot judge: every other one is still granted, and the listing names it.
addRepo("jschoubben/after-mute", true);
const pass = await readAll.grant();
assert.ok(pass.granted.includes("jschoubben/after-mute"), "the pass did not stop at the repository it could not judge");
assert.ok(pass.granted.some((x: any) => x.repository === "jschoubben/after-mute" && x.account === BUILDER_ACCOUNT), "the pass did not stop at the repository it could not judge");
assert.ok(pass.refused.some((x: any) => x.repository === "jschoubben/mute" && /permission could not be read/.test(x.why)));
const listed = await readAllTools(() => readAll)[0].run({}) as any;
assert.equal(listed.unreadable.find((u: any) => u.repository === "jschoubben/mute").permission, "unknown");
+9
View File
@@ -22,6 +22,15 @@
import { keepMainRules } from "./mainrules.js";
export const TOOLS_ACCOUNT = "mesh-tools";
/** The build modules' account, whose keys fetch over ssh (internal/forgeaccount BuilderUser, ADR 0466 §3). */
export const BUILDER_ACCOUNT = "nox-builder";
/** The machines' agents' account, whose keys ssh-client reports (internal/forgeaccount AgentUser, ADR 0410 §1). */
export const AGENT_ACCOUNT = "nox-agent";
/** Every forge account the mesh's own parts use: each reads every repository on the forge (readall.ts; the operator,
* 2026-10-26). internal/forgeaccount's test holds the two Go names to its own constants. */
export function meshAccounts(tools: string = TOOLS_ACCOUNT): string[] {
return [tools, BUILDER_ACCOUNT, AGENT_ACCOUNT];
}
/** The organisation whose repositories are the mesh's, and the team through which the tools' account writes there. */
export const TOOLS_ORG = "novox";
export const TOOLS_TEAM = "mesh-tools";
@@ -13,6 +13,8 @@ import (
"path/filepath"
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// The runners of a digest or test step: this installed program, or the candidate's compiled from the workspace.
@@ -108,7 +110,7 @@ func (m *Module) mergeCommit(ctx context.Context, workspace string) (string, err
r := m.Run(ctx, nil, "git", "-C", filepath.Join(m.Work, workspace, "tree"), "rev-parse", "HEAD")
commit := strings.TrimSpace(r.Stdout)
if r.Status != 0 || r.Err != "" || commit == "" {
return "", fmt.Errorf("the workspace's commit: git rev-parse: %s", firstLine(r.Err+"\n"+r.Stderr))
return "", fmt.Errorf("the workspace's commit: git rev-parse: %s", forgekey.Said(r.Err+"\n"+r.Stderr))
}
return commit, nil
}
@@ -18,6 +18,7 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
var commitOK = regexp.MustCompile(`^[0-9a-f]{40}$`)
@@ -255,7 +256,7 @@ func (m *Module) downloadAndVerify(ctx context.Context, dir, d string) error {
func (m *Module) goModDirs(ctx context.Context, tree string) ([]string, error) {
r := m.Run(ctx, nil, "git", "-C", tree, "ls-files", "--", "go.mod", "*/go.mod")
if r.Status != 0 || r.Err != "" {
return nil, fmt.Errorf("git ls-files: %s", firstLine(r.Err+" "+r.Stderr))
return nil, fmt.Errorf("git ls-files: %s", forgekey.Said(r.Err+"\n"+r.Stderr))
}
dirs := []string{}
for _, l := range strings.Split(r.Stdout, "\n") {
@@ -9,6 +9,7 @@ import (
"syscall"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// mirrorLocks hold one mutex per mirror of this process; the lock file beside the mirror holds off another process.
@@ -67,7 +68,7 @@ func (m *Module) dissociate(ctx context.Context, dir string) error {
return nil // no mirror was used
}
if r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-C", dir, "repack", "-a", "-d", "-q"); r.Status != 0 || r.Err != "" {
return fmt.Errorf("git repack in %s: %s", dir, firstLine(r.Err+" "+r.Stderr))
return fmt.Errorf("git repack in %s: %s", dir, forgekey.Said(r.Err+"\n"+r.Stderr))
}
return os.Remove(alternates)
}
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"os"
"os/exec"
@@ -121,20 +122,35 @@ func TestTwoUpdatesOfOneMirrorNeverRunAtOnce(t *testing.T) {
}
}
// A mirror that cannot be updated does not fail the fetch: the fetch goes on without it.
// A mirror that cannot be updated does not fail the fetch: the fetch goes on without it, and the line saying so carries
// git's own words. The forge's refusal of a repository its account cannot read came out as "git -C: error:" (ace,
// 2026-10-26: git writes a bare "error:" line before the forge's message).
func TestAMirrorThatCannotBeUpdatedLetsTheFetchGoOnWithoutIt(t *testing.T) {
f := &fakeStep{gomods: "go.mod\n"}
f.rule = func(line string) (r runAnswer, ok bool) {
if strings.Contains(line, "mirror") && strings.Contains(line, " fetch ") {
return runAnswer{Status: 1, Stderr: "fatal: no space left\n"}, true
return runAnswer{Status: 128, Stderr: "error: \nerror: User: 22:nox-builder with Key: 30:mesh:ace.mesh-build-go is not authorized to read jschoubben/car-hunter.\nerror: \nfatal: Could not read from remote repository.\n"}, true
}
return r, false
}
m, states := fetchModule(t, f)
var mu sync.Mutex
var said []string
m.Say = func(format string, a ...any) {
mu.Lock()
defer mu.Unlock()
said = append(said, fmt.Sprintf(format, a...))
}
ws := fetchOnce(t, m, states, fetchArgs).Workspace
if _, err := os.Stat(filepath.Join(m.Work, ws, "tree", ".git", "objects", "info", "alternates")); err == nil {
t.Errorf("the workspace borrows from a mirror that is not there")
}
mu.Lock()
defer mu.Unlock()
all := strings.Join(said, "\n")
if !strings.Contains(all, "is not used: git fetch: error: User: 22:nox-builder with Key: 30:mesh:ace.mesh-build-go is not authorized to read jschoubben/car-hunter.; fatal: Could not read from remote repository.") {
t.Errorf("the mirror's line lost git's words:\n%s", all)
}
}
func sleepABit() { time.Sleep(30 * time.Millisecond) }
@@ -15,6 +15,8 @@ import (
"path/filepath"
"regexp"
"strings"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
const forgePrefix = "git.novox.be/novox/"
@@ -125,7 +127,7 @@ func (m *Module) placePin(ctx context.Context, root string, p pin, failure strin
repository := "novox/" + p.repo
r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-c", "credential.helper=", "ls-remote", m.remote(repository), "refs/pull/*/head")
if r.Status != 0 || r.Err != "" {
return "", fmt.Errorf("the pin of %s at commit %s: listing the pull requests of %s: %s", p.module, p.short, repository, firstLine(r.Err+" "+r.Stderr))
return "", fmt.Errorf("the pin of %s at commit %s: listing the pull requests of %s: %s", p.module, p.short, repository, forgekey.Said(r.Err+"\n"+r.Stderr))
}
commit, ref := "", ""
for _, l := range strings.Split(r.Stdout, "\n") {
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildgo
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d h1:7F/dFerm01wvuGLPgJ67/VfdcvDexc+BQEE3qzOrlu4=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildimage
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d h1:7F/dFerm01wvuGLPgJ67/VfdcvDexc+BQEE3qzOrlu4=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -212,7 +212,7 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, grou
func (m *Module) typeScriptModules(ctx context.Context, tree string) (dirs []string, packages map[string]bool, err error) {
r := m.Run(ctx, m.baseEnv(), "git", "-C", tree, "ls-files", "--", "module.json", "*/module.json")
if r.Status != 0 || r.Err != "" {
return nil, nil, fmt.Errorf("git ls-files: %s", firstLine(r.Err+" "+r.Stderr))
return nil, nil, fmt.Errorf("git ls-files: %s", forgekey.Said(r.Err+"\n"+r.Stderr))
}
packages = map[string]bool{}
for _, l := range strings.Split(r.Stdout, "\n") {
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildnode
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d h1:7F/dFerm01wvuGLPgJ67/VfdcvDexc+BQEE3qzOrlu4=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=