a73x

151b3f0f

Hoist the delegate ceiling above the governance-state match

a73x   2026-08-18 17:26

Commit message
Hoist the delegate ceiling above the governance-state match

The ceiling lived inside `if let GovernanceState::Active`, so a hook
invocation that re-reads settings as Absent — the settings repository
removed between the session's regime check and this re-read — skipped
it entirely, leaving a delegate free to write refs/heads/*. Moved it
above the match: ENV_DELEGATE is set only by the server, only for a
delegate session, so its presence needs no governance state to be a
sufficient trigger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/server/governance/hook.rs
Old New
@@ -122,6 +122,23 @@ pub fn run(refname: &str, old: &str, new: &str) -> Result<(), String> {
122 // Empty is legitimate: an ungoverned server has no names to pass. 122 // Empty is legitimate: an ungoverned server has no names to pass.
123 let principal = std::env::var(ENV_PRINCIPAL).unwrap_or_default(); 123 let principal = std::env::var(ENV_PRINCIPAL).unwrap_or_default();
124 124
125 // The delegate ceiling. Checked ahead of and independent of governance
126 // state: ENV_DELEGATE is set only by the server, only for a delegate
127 // session, so its presence alone is a sufficient trigger. Nesting this
128 // inside `GovernanceState::Active` would let a settings repository that
129 // goes briefly unreadable-as-Absent between the session's regime check
130 // and this hook's own re-read skip the ceiling entirely — hard-coded
131 // rather than configured, so no line in access.conf can widen it either.
132 let delegate = std::env::var(ENV_DELEGATE).ok().filter(|v| !v.is_empty());
133 if let Some(key_id) = &delegate {
134 if !refname.starts_with("refs/collab/") {
135 return Err(format!(
136 "delegate {key_id} of {principal} may only write refs/collab/*, \
137 not {refname}"
138 ));
139 }
140 }
141
125 let old = parse_oid(old)?; 142 let old = parse_oid(old)?;
126 let new = parse_oid(new)?; 143 let new = parse_oid(new)?;
127 144
@@ -138,17 +155,6 @@ pub fn run(refname: &str, old: &str, new: &str) -> Result<(), String> {
138 if principal.is_empty() { 155 if principal.is_empty() {
139 return Err("no authenticated principal on this push".to_string()); 156 return Err("no authenticated principal on this push".to_string());
140 } 157 }
141 // The delegate ceiling. Hard-coded rather than configured: no line in
142 // access.conf can widen what a certificate may write.
143 let delegate = std::env::var(ENV_DELEGATE).ok().filter(|v| !v.is_empty());
144 if let Some(key_id) = &delegate {
145 if !refname.starts_with("refs/collab/") {
146 return Err(format!(
147 "delegate {key_id} of {principal} may only write refs/collab/*, \
148 not {refname}"
149 ));
150 }
151 }
152 158
153 let creator = creator_of(&repo_path); 159 let creator = creator_of(&repo_path);
154 let subject = Subject::with_creator(&principal, creator.as_deref()); 160 let subject = Subject::with_creator(&principal, creator.as_deref());