From 7676e67c5e98f6b86c19e24d492cf744c76b8981 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 12:02:00 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Trust=20a=20module's=20trunk=20for=20who=20?= =?UTF-8?q?can=20merge=20into=20it,=20not=20for=20a=20required=20status=20?= =?UTF-8?q?(ADR=200534=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The forge requires no status to merge any more (ADR 0534 §1); a trunk is trusted when it takes no push, its merge whitelist names mesh-tools alone and no administrator merges past its rule, so only mesh-delivery lands a commit there, on a passing mesh/check and the operator's Approve. The old rule failed the 11:51 delivery plan once mesh-catalog's status was unticked. --- cmd/mesh-controller/build_source.go | 25 +++++++++++---- cmd/mesh-controller/build_source_test.go | 39 +++++++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/cmd/mesh-controller/build_source.go b/cmd/mesh-controller/build_source.go index 82f29a7b..1a29a790 100644 --- a/cmd/mesh-controller/build_source.go +++ b/cmd/mesh-controller/build_source.go @@ -205,8 +205,9 @@ func (f *sourceForms) buildsFrom(entries []inventory.Entry, repository, seat str type forgeFacts struct { // ID is the forge's own id of the repository: what tells it from one deleted and made again by its name. ID int64 - // Guarded is whether the branch is protected as a module's trunk must be: no direct push, at least one - // required status, and no administrator merging past one. Why says what is missing when it is not. + // Guarded is whether the branch is protected as a module's trunk must be: no direct push, a merge whitelist naming + // the tools' account alone, and no administrator merging past the rule (novox/hq ADR 0534 §6; no required status, + // which the forge no longer asks, §1). Why says what is missing when it is not. Guarded bool Why string } @@ -361,27 +362,39 @@ func askTheForgeOnce(ctx context.Context, owner, repo, branch string) (forgeFact return facts, nil } +// forgeToolsAccount is the forge tools' account, the only one main's merge whitelist names (novox/hq ADR 0398 §2, the +// gitea module's tools-account.ts TOOLS_ACCOUNT): only mesh-delivery, through the merge tool, lands a commit on a trunk. +const forgeToolsAccount = "mesh-tools" + // protectionRule is a branch protection rule as the forge's tool summarises it. type protectionRule struct { Rule string `json:"rule"` Push bool `json:"push"` RequiredStatuses []string `json:"required_statuses"` AdminMayOverride bool `json:"admin_may_override"` + // MergeWhitelist is who may merge into the branch; nil when the rule keeps no merge whitelist (anyone who writes). + MergeWhitelist []string `json:"merge_whitelist"` } // judgedRule judges the one rule the forge applies to a branch, as the forge picks it: the rule named for the // branch, else the first glob rule, in the forge's order, that covers it (gitea's first matching rule). Never a -// later rule that happens to be stronger: the forge does not read it. +// later rule that happens to be stronger: the forge does not read it. A trunk is trusted for who can land a commit on +// it, not for a status it requires (novox/hq ADR 0534 §6): no push, only the tools' account merges, and no administrator +// merging past the rule; then only mesh-delivery lands a commit there, through the merge tool, on a passing mesh/check +// and the operator's Approve (ADR 0534 §4). func judgedRule(rules []protectionRule, branch string) forgeFacts { switch r := appliedRule(rules, branch); { case r == nil: return forgeFacts{Why: fmt.Sprintf("no protection rule covers %s", branch)} case r.Push: return forgeFacts{Why: fmt.Sprintf("the rule %s lets a person push to %s directly", r.Rule, branch)} - case len(r.RequiredStatuses) == 0: - return forgeFacts{Why: fmt.Sprintf("the rule %s requires no status before a merge into %s", r.Rule, branch)} + case r.MergeWhitelist == nil || len(r.MergeWhitelist) == 0: + return forgeFacts{Why: fmt.Sprintf("the rule %s keeps no merge whitelist naming %s alone for %s", r.Rule, forgeToolsAccount, branch)} + case len(r.MergeWhitelist) != 1 || r.MergeWhitelist[0] != forgeToolsAccount: + return forgeFacts{Why: fmt.Sprintf("the rule %s lets %s merge into %s, not %s alone", r.Rule, + strings.Join(r.MergeWhitelist, ", "), branch, forgeToolsAccount)} case r.AdminMayOverride: - return forgeFacts{Why: fmt.Sprintf("the rule %s lets an administrator merge into %s past a status", r.Rule, branch)} + return forgeFacts{Why: fmt.Sprintf("the rule %s lets an administrator merge into %s past it", r.Rule, branch)} } return forgeFacts{Guarded: true} } diff --git a/cmd/mesh-controller/build_source_test.go b/cmd/mesh-controller/build_source_test.go index 1f692d8b..36eafb6c 100644 --- a/cmd/mesh-controller/build_source_test.go +++ b/cmd/mesh-controller/build_source_test.go @@ -102,16 +102,17 @@ func init() { // The forge applies one rule to a branch: the rule named for it, else the first glob that covers it. A stronger // rule later in the list is not what guards the branch, and is not read as if it were (the confirmation review). func TestTheRuleJudgedIsTheOneTheForgeApplies(t *testing.T) { - guarded := protectionRule{Rule: "*", RequiredStatuses: []string{"mesh/merge-gate"}} - open := protectionRule{Rule: "main", Push: true, RequiredStatuses: []string{"mesh/merge-gate"}} + tools := []string{forgeToolsAccount} + guarded := protectionRule{Rule: "*", MergeWhitelist: tools} + open := protectionRule{Rule: "main", Push: true, MergeWhitelist: tools} if f := judgedRule([]protectionRule{guarded, open}, "main"); f.Guarded { t.Error("an exact rule letting pushes was passed over for a glob that guards") } - if f := judgedRule([]protectionRule{{Rule: "ma*", RequiredStatuses: nil}, {Rule: "*", RequiredStatuses: []string{"x"}}}, + if f := judgedRule([]protectionRule{{Rule: "ma*", MergeWhitelist: nil}, {Rule: "*", MergeWhitelist: tools}}, "main"); f.Guarded || !strings.Contains(f.Why, "ma*") { t.Errorf("the first glob covering the branch was not the one judged: %+v", f) } - if f := judgedRule([]protectionRule{{Rule: "release/*"}, {Rule: "main", RequiredStatuses: []string{"x"}}}, "main"); !f.Guarded { + if f := judgedRule([]protectionRule{{Rule: "release/*"}, {Rule: "main", MergeWhitelist: tools}}, "main"); !f.Guarded { t.Errorf("the rule named for the branch was not judged: %+v", f) } if f := judgedRule(nil, "main"); f.Guarded { @@ -119,6 +120,36 @@ func TestTheRuleJudgedIsTheOneTheForgeApplies(t *testing.T) { } } +// **A module's trunk is trusted when it takes no push, only the tools' account merges into it, and no administrator +// merges past its rule** (novox/hq ADR 0534 §6): the forge requires no status to merge (§1), so a rule requiring none is +// trusted; one with no merge whitelist, another name in it, a push allowed or an administrator's override is not. +func TestATrunkIsTrustedForItsMergersNotForAStatus(t *testing.T) { + tools := []string{forgeToolsAccount} + for _, c := range []struct { + name string + rule protectionRule + want bool + why string + }{ + {"no status, no push, the tools alone merge", protectionRule{Rule: "main", MergeWhitelist: tools}, true, ""}, + {"a status required as well", protectionRule{Rule: "main", MergeWhitelist: tools, RequiredStatuses: []string{"mesh/check"}}, true, ""}, + {"no merge whitelist", protectionRule{Rule: "main"}, false, "merge whitelist"}, + {"an empty merge whitelist", protectionRule{Rule: "main", MergeWhitelist: []string{}}, false, "merge whitelist"}, + {"another name in the whitelist", protectionRule{Rule: "main", MergeWhitelist: []string{forgeToolsAccount, "agent-ace"}}, false, "agent-ace"}, + {"another name alone", protectionRule{Rule: "main", MergeWhitelist: []string{"agent-ace"}}, false, "agent-ace"}, + {"a push allowed", protectionRule{Rule: "main", Push: true, MergeWhitelist: tools}, false, "push"}, + {"an administrator's override", protectionRule{Rule: "main", MergeWhitelist: tools, AdminMayOverride: true}, false, "administrator"}, + } { + f := judgedRule([]protectionRule{c.rule}, "main") + if f.Guarded != c.want || !strings.Contains(f.Why, c.why) { + t.Errorf("%s: %+v, want guarded %v saying %q", c.name, f, c.want, c.why) + } + if strings.Contains(f.Why, "requires no status") { + t.Errorf("%s: a trunk is still judged on a required status: %s", c.name, f.Why) + } + } +} + // An id the forge could not give keeps the id already recorded. // **The operator's Approve of a reviewed group registers a moved module from its new repository** (novox/hq ADR 0420 §7, From b2af3d654f457169f0f784bf9bca418d44207298 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 12:10:11 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Trust=20a=20trunk=20whose=20whitelist=20add?= =?UTF-8?q?s=20the=20operator's=20own=20login,=20never=20an=20agent's=20(A?= =?UTF-8?q?DR=200534=20=C2=A72,=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gitea module keeps the operator's login beside mesh-tools on every main so the operator can merge by hand; an agent's account there is still refused. --- cmd/mesh-controller/build_source.go | 28 ++++++++++++++++-------- cmd/mesh-controller/build_source_test.go | 13 ++++++----- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/cmd/mesh-controller/build_source.go b/cmd/mesh-controller/build_source.go index 1a29a790..31504e33 100644 --- a/cmd/mesh-controller/build_source.go +++ b/cmd/mesh-controller/build_source.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "regexp" + "slices" "strings" "time" @@ -206,8 +207,8 @@ type forgeFacts struct { // ID is the forge's own id of the repository: what tells it from one deleted and made again by its name. ID int64 // Guarded is whether the branch is protected as a module's trunk must be: no direct push, a merge whitelist naming - // the tools' account alone, and no administrator merging past the rule (novox/hq ADR 0534 §6; no required status, - // which the forge no longer asks, §1). Why says what is missing when it is not. + // the tools' account and no agent's account, and no administrator merging past the rule (novox/hq ADR 0534 §6; no + // required status, which the forge no longer asks, §1). Why says what is missing when it is not. Guarded bool Why string } @@ -362,10 +363,15 @@ func askTheForgeOnce(ctx context.Context, owner, repo, branch string) (forgeFact return facts, nil } -// forgeToolsAccount is the forge tools' account, the only one main's merge whitelist names (novox/hq ADR 0398 §2, the -// gitea module's tools-account.ts TOOLS_ACCOUNT): only mesh-delivery, through the merge tool, lands a commit on a trunk. +// forgeToolsAccount is the forge tools' account, which main's merge whitelist names for mesh-delivery (novox/hq ADR 0398 +// §2, the gitea module's tools-account.ts TOOLS_ACCOUNT); beside it the whitelist names only people's own logins, the +// operator's (ADR 0534 §2), never an agent's account. const forgeToolsAccount = "mesh-tools" +// isAgentAccount is whether a forge login is a node's agent's account, agent- (novox/hq ADR 0398 §1): one that +// may never merge into a module's trunk by itself. +func isAgentAccount(login string) bool { return strings.HasPrefix(login, "agent-") } + // protectionRule is a branch protection rule as the forge's tool summarises it. type protectionRule struct { Rule string `json:"rule"` @@ -379,9 +385,10 @@ type protectionRule struct { // judgedRule judges the one rule the forge applies to a branch, as the forge picks it: the rule named for the // branch, else the first glob rule, in the forge's order, that covers it (gitea's first matching rule). Never a // later rule that happens to be stronger: the forge does not read it. A trunk is trusted for who can land a commit on -// it, not for a status it requires (novox/hq ADR 0534 §6): no push, only the tools' account merges, and no administrator -// merging past the rule; then only mesh-delivery lands a commit there, through the merge tool, on a passing mesh/check -// and the operator's Approve (ADR 0534 §4). +// it, not for a status it requires (novox/hq ADR 0534 §6): no push, a merge whitelist naming the tools' account and +// beside it only people's own logins, never an agent's, and no administrator merging past the rule; then a commit lands +// there through mesh-delivery, on a passing mesh/check and the operator's Approve (ADR 0534 §4), or by the operator's +// own hand at the forge (§2). func judgedRule(rules []protectionRule, branch string) forgeFacts { switch r := appliedRule(rules, branch); { case r == nil: @@ -390,9 +397,12 @@ func judgedRule(rules []protectionRule, branch string) forgeFacts { return forgeFacts{Why: fmt.Sprintf("the rule %s lets a person push to %s directly", r.Rule, branch)} case r.MergeWhitelist == nil || len(r.MergeWhitelist) == 0: return forgeFacts{Why: fmt.Sprintf("the rule %s keeps no merge whitelist naming %s alone for %s", r.Rule, forgeToolsAccount, branch)} - case len(r.MergeWhitelist) != 1 || r.MergeWhitelist[0] != forgeToolsAccount: - return forgeFacts{Why: fmt.Sprintf("the rule %s lets %s merge into %s, not %s alone", r.Rule, + case !slices.Contains(r.MergeWhitelist, forgeToolsAccount): + return forgeFacts{Why: fmt.Sprintf("the rule %s lets %s merge into %s, and not %s", r.Rule, strings.Join(r.MergeWhitelist, ", "), branch, forgeToolsAccount)} + case slices.ContainsFunc(r.MergeWhitelist, isAgentAccount): + return forgeFacts{Why: fmt.Sprintf("the rule %s lets an agent's account merge into %s: %s", r.Rule, branch, + strings.Join(r.MergeWhitelist, ", "))} case r.AdminMayOverride: return forgeFacts{Why: fmt.Sprintf("the rule %s lets an administrator merge into %s past it", r.Rule, branch)} } diff --git a/cmd/mesh-controller/build_source_test.go b/cmd/mesh-controller/build_source_test.go index 36eafb6c..7a3a7eff 100644 --- a/cmd/mesh-controller/build_source_test.go +++ b/cmd/mesh-controller/build_source_test.go @@ -120,9 +120,10 @@ func TestTheRuleJudgedIsTheOneTheForgeApplies(t *testing.T) { } } -// **A module's trunk is trusted when it takes no push, only the tools' account merges into it, and no administrator -// merges past its rule** (novox/hq ADR 0534 §6): the forge requires no status to merge (§1), so a rule requiring none is -// trusted; one with no merge whitelist, another name in it, a push allowed or an administrator's override is not. +// **A module's trunk is trusted when it takes no push, only the tools' account and people's own logins merge into it, +// and no administrator merges past its rule** (novox/hq ADR 0534 §2, §6): the forge requires no status to merge (§1), so +// a rule requiring none is trusted; one with no merge whitelist, no tools' account in it, an agent's account in it, a +// push allowed or an administrator's override is not. func TestATrunkIsTrustedForItsMergersNotForAStatus(t *testing.T) { tools := []string{forgeToolsAccount} for _, c := range []struct { @@ -135,8 +136,10 @@ func TestATrunkIsTrustedForItsMergersNotForAStatus(t *testing.T) { {"a status required as well", protectionRule{Rule: "main", MergeWhitelist: tools, RequiredStatuses: []string{"mesh/check"}}, true, ""}, {"no merge whitelist", protectionRule{Rule: "main"}, false, "merge whitelist"}, {"an empty merge whitelist", protectionRule{Rule: "main", MergeWhitelist: []string{}}, false, "merge whitelist"}, - {"another name in the whitelist", protectionRule{Rule: "main", MergeWhitelist: []string{forgeToolsAccount, "agent-ace"}}, false, "agent-ace"}, - {"another name alone", protectionRule{Rule: "main", MergeWhitelist: []string{"agent-ace"}}, false, "agent-ace"}, + {"the operator's own login beside the tools", protectionRule{Rule: "main", MergeWhitelist: []string{forgeToolsAccount, "jschoubben"}}, true, ""}, + {"an agent's account in the whitelist", protectionRule{Rule: "main", MergeWhitelist: []string{forgeToolsAccount, "agent-ace"}}, false, "agent-ace"}, + {"an agent's account alone", protectionRule{Rule: "main", MergeWhitelist: []string{"agent-ace"}}, false, "agent-ace"}, + {"a person's login without the tools", protectionRule{Rule: "main", MergeWhitelist: []string{"jschoubben"}}, false, forgeToolsAccount}, {"a push allowed", protectionRule{Rule: "main", Push: true, MergeWhitelist: tools}, false, "push"}, {"an administrator's override", protectionRule{Rule: "main", MergeWhitelist: tools, AdminMayOverride: true}, false, "administrator"}, } {