fix(#1953): route approval helper-events to the submitter, not the root agent

This commit is contained in:
damocles 2026-06-23 20:09:40 +02:00 committed by mara
commit 3618399d94
7 changed files with 187 additions and 73 deletions

View file

@ -70,6 +70,21 @@ fn ensure_fetched_sha_column(conn: &Connection) -> Result<()> {
Ok(())
}
/// Same shape as `ensure_fetched_sha_column` but for `submitter` — the
/// agent that submitted the approval (the authenticated socket caller).
/// Approval-scoped helper events route to this agent. Legacy rows have
/// NULL; callers fall back to the root agent for those.
fn ensure_submitter_column(conn: &Connection) -> Result<()> {
let has: bool = conn
.prepare("SELECT 1 FROM pragma_table_info('approvals') WHERE name = 'submitter'")?
.exists([])?;
if !has {
conn.execute_batch("ALTER TABLE approvals ADD COLUMN submitter TEXT;")
.context("add approvals.submitter column")?;
}
Ok(())
}
pub struct Approvals {
conn: Mutex<Connection>,
}
@ -87,6 +102,7 @@ impl Approvals {
ensure_kind_column(&conn).context("migrate approvals.kind")?;
ensure_fetched_sha_column(&conn).context("migrate approvals.fetched_sha")?;
ensure_description_column(&conn).context("migrate approvals.description")?;
ensure_submitter_column(&conn).context("migrate approvals.submitter")?;
Ok(Self {
conn: Mutex::new(conn),
})
@ -98,22 +114,41 @@ impl Approvals {
kind: ApprovalKind,
commit_ref: &str,
description: Option<&str>,
submitter: &str,
) -> Result<i64> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT INTO approvals (agent, kind, commit_ref, requested_at, status, description)
VALUES (?1, ?2, ?3, ?4, 'pending', ?5)",
"INSERT INTO approvals
(agent, kind, commit_ref, requested_at, status, description, submitter)
VALUES (?1, ?2, ?3, ?4, 'pending', ?5, ?6)",
params![
agent,
kind_to_str(kind),
commit_ref,
now_unix(),
description
description,
submitter
],
)?;
Ok(conn.last_insert_rowid())
}
/// The agent that submitted approval `id` (the authenticated socket
/// caller at submit time). `None` for legacy rows predating the
/// `submitter` column — callers route those to the root agent.
pub fn submitter_of(&self, id: i64) -> Result<Option<String>> {
let conn = self.conn.lock().unwrap();
let submitter: Option<String> = conn
.query_row(
"SELECT submitter FROM approvals WHERE id = ?1",
params![id],
|row| row.get(0),
)
.optional()?
.flatten();
Ok(submitter)
}
/// Record the canonical sha hive-c0re fetched from the proposed repo
/// into applied at submission time. Idempotent on identical values.
pub fn set_fetched_sha(&self, id: i64, sha: &str) -> Result<()> {
@ -428,7 +463,13 @@ mod tests {
// approval then vanished from the dashboard.
let (_dir, _path, db) = open_temp();
let id = db
.submit_kind("bitburner", ApprovalKind::InitConfig, "", Some("scaffold"))
.submit_kind(
"bitburner",
ApprovalKind::InitConfig,
"",
Some("scaffold"),
"bitburner",
)
.expect("submit init_config");
let pending = db
.pending()
@ -441,10 +482,11 @@ mod tests {
#[test]
fn mixed_kinds_all_listed() {
let (_dir, _path, db) = open_temp();
db.submit_kind("a", ApprovalKind::ApplyCommit, "deadbeef", None)
db.submit_kind("a", ApprovalKind::ApplyCommit, "deadbeef", None, "a")
.unwrap();
db.submit_kind("b", ApprovalKind::Spawn, "", None).unwrap();
db.submit_kind("c", ApprovalKind::InitConfig, "", None)
db.submit_kind("b", ApprovalKind::Spawn, "", None, "b")
.unwrap();
db.submit_kind("c", ApprovalKind::InitConfig, "", None, "c")
.unwrap();
let pending = db.pending().expect("pending");
assert_eq!(pending.len(), 3, "all three kinds must be visible");
@ -462,6 +504,7 @@ mod tests {
ApprovalKind::ApplyCommit,
"cafef00d",
Some("test"),
"bitburner",
)
.unwrap();
let row = db.mark_cancelled(id, "manager").expect("cancel");
@ -481,7 +524,7 @@ mod tests {
// final — re-cancelling errors instead of silently overwriting.
let (_dir, _path, db) = open_temp();
let id = db
.submit_kind("a", ApprovalKind::Spawn, "deadbeef", None)
.submit_kind("a", ApprovalKind::Spawn, "deadbeef", None, "a")
.unwrap();
db.mark_cancelled(id, "manager").expect("first cancel");
let err = db
@ -496,7 +539,7 @@ mod tests {
// whole list — collect_lenient skips it instead of failing.
let (_dir, path, db) = open_temp();
let good = db
.submit_kind("good", ApprovalKind::ApplyCommit, "cafe", None)
.submit_kind("good", ApprovalKind::ApplyCommit, "cafe", None, "good")
.unwrap();
let raw = Connection::open(&path).unwrap();
raw.execute(
@ -511,4 +554,26 @@ mod tests {
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].id, good);
}
#[test]
fn submitter_round_trips_and_legacy_reads_none() {
// A submitted approval records its submitter; a legacy row
// (inserted without the column) reads back as None so callers
// fall back to the root agent.
let (_dir, path, db) = open_temp();
let id = db
.submit_kind("child", ApprovalKind::ApplyCommit, "cafe", None, "parent")
.unwrap();
assert_eq!(db.submitter_of(id).unwrap().as_deref(), Some("parent"));
let raw = Connection::open(&path).unwrap();
raw.execute(
"INSERT INTO approvals (agent, kind, commit_ref, requested_at, status)
VALUES ('old', 'apply_commit', '', 0, 'pending')",
[],
)
.unwrap();
let legacy_id = raw.last_insert_rowid();
assert_eq!(db.submitter_of(legacy_id).unwrap(), None);
}
}