Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ Deployment-root community management uses operator-signed NIP-98 HTTP requests.
`POST /operator/communities/delete` accepts only an exact normalized, archived
community whose asserted pubkey is still its owner. The caller supplies the
request UUID as the stable correlation/idempotency identity; the durable row
records owner intent, mediating operator, and acknowledgement version. Admission
returns `202` at the `submitted` stage and performs no inventory, approval,
records operator-attested owner intent, mediating operator, and acknowledgement
version. Admission returns `202` at the `submitted` stage and performs no
inventory, approval,
quiescing, object-store access, or deletion execution synchronously. While that
non-aborted request exists, unarchive and ownership transfer conflict and owner
management lists suppress the archived row. Replaying the same UUID converges
Expand All @@ -45,6 +46,19 @@ on the row. Owner provenance pins `requested_by` to `owner_pubkey`, so passing
the operator's own pubkey does not converge — it conflicts with the existing
one-active-request invariant instead.

The privileged one-shot `buzz-admin deletions drain` process gives already-
approved work priority. When none is ready, it may claim only an
operator-attested owner-origin `submitted` request under the same durable
generation lease used for execution, inventory it with lease-loss cancellation,
and atomically freeze the inventory plus a digest-bound `owner_automatic`
approval. The mediating operator remains the approval actor; the owner
acknowledgement is pre-inventory
intent, not a claim that the owner reviewed the digest. The retained lease then
enters the unchanged approved-request executor. Operator-origin requests never
auto-progress and still require explicit inventory and approval. Owner
admission still has no owner-facing cancellation or grace period; the
privileged abort described above remains the recovery path.

Ownership is mutable only while a community is active. Archiving freezes the
current owner. Normal transfer and deployment-root legacy convergence take the
same community-row lock as owner-deletion admission, then reject archived,
Expand Down Expand Up @@ -762,6 +776,10 @@ Postgres/Redis clients and S3 client; it does not call relay HTTP. Durable
requests, leases, retry timing, and checkpoints in Postgres are the handoff and
execution authority, so Kubernetes uses `Forbid` concurrency and zero Job
retries rather than introducing a second retry system.
The same drain first claims runnable approved work and, only when none exists,
may prepare one owner-origin submission. Inventory, automatic approval, and
execution share one generation lease and the existing retry/block/checkpoint
records; there is no preparation worker, command, queue, or retry authority.

---

Expand Down
77 changes: 75 additions & 2 deletions crates/buzz-db/src/runtime/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,9 +705,12 @@ mod postgres_tests {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);

assert_eq!(migrations.len(), 52);
assert_eq!(migrations.len(), 53);
assert_eq!(migrations[48].version, 49);
assert_eq!(migrations[49].version, 50);
assert_eq!(migrations[50].version, 51);
assert_eq!(migrations[51].version, 52);
assert_eq!(migrations[52].version, 53);
assert!(migrations[48]
.sql
.as_str()
Expand All @@ -716,6 +719,11 @@ mod postgres_tests {
.sql
.as_str()
.contains("community_deletion_owner_provenance"));
assert!(migrations[52].sql.as_str().contains("approval_origin"));
assert!(migrations[52]
.sql
.as_str()
.contains("community_deletion_requests_owner_preparable"));
assert_eq!(migrations[0].version, 1);
assert_eq!(&*migrations[0].description, "initial schema");
assert!(migrations[0]
Expand Down Expand Up @@ -1722,6 +1730,34 @@ mod postgres_tests {
);
}

#[test]
fn owner_deletion_auto_approval_migration_matches_desired_schema() {
let migration = MIGRATOR
.iter()
.find(|migration| migration.version == 53)
.expect("embedded migration 0053")
.sql
.as_ref()
.to_ascii_lowercase();
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.expect("workspace root");
let schema = std::fs::read_to_string(workspace_root.join("schema/schema.sql"))
.expect("read schema/schema.sql")
.to_ascii_lowercase();

for sql in [&migration, &schema] {
assert!(sql.contains("approval_origin text not null default 'operator'"));
assert!(sql.contains("approval_origin in ('operator', 'owner_automatic')"));
assert!(sql.contains("'submitted', 'approved', 'fenced'"));
assert!(sql.contains("community_deletion_requests_owner_preparable"));
assert!(sql.contains("request_origin = 'owner'"));
assert!(sql.contains("stage = 'submitted'"));
}
assert!(migration.contains("set local lock_timeout = '5s'"));
}

/// Structural parity between migration 0029's deletion surface and the
/// desired-state bootstrap schema (`schema/schema.sql`).
///
Expand Down Expand Up @@ -1866,13 +1902,50 @@ mod postgres_tests {
.tables
.get(table)
.unwrap_or_else(|| panic!("schema.sql is missing deletion table {table}"));
if table != "community_deletion_requests" {
if table != "community_deletion_requests" && table != "community_deletion_approvals" {
assert_eq!(
in_schema, definition,
"schema.sql definition of {table} drifted from migration 0029"
);
}
}
let migration_approval_table = migration
.tables
.get("community_deletion_approvals")
.expect("0029 approval table");
let schema_approval_table = schema
.tables
.get("community_deletion_approvals")
.expect("schema.sql approval table");
for invariant in [
"inventory_digest bytea not null check (length(inventory_digest) = 32)",
"foreign key (request_id, community_id, inventory_digest) references community_deletion_requests(id, community_id, inventory_digest) on delete restrict",
] {
assert!(
migration_approval_table.contains(invariant),
"0029 deletion approvals are missing {invariant}"
);
assert!(
schema_approval_table.contains(invariant),
"schema.sql deletion approvals are missing {invariant}"
);
}
let migration_request_table = migration
.tables
.get("community_deletion_requests")
.expect("0029 deletion request table");
for request_table in [
migration_request_table,
schema
.tables
.get("community_deletion_requests")
.expect("schema.sql deletion request table"),
] {
assert!(
request_table.contains("unique (id, community_id, inventory_digest)"),
"deletion requests must expose the exact composite approval target"
);
}
for (function, definition) in &migration.functions {
let in_schema = schema
.functions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ async fn migration_schema_thread_window_prebuild_does_not_queue_behind_writer()
production_result.is_ok(),
"production migrator must preserve ingestion progress: {production_result:?}"
);
assert_eq!(version, 52);
assert_eq!(version, 53);
assert_eq!(final_oid, oid, "prebuild must not be replaced");
assert_eq!(count, 4, "all writer witnesses must persist");
}
Loading
Loading