mirror of
https://github.com/block/buzz.git
synced 2026-11-05 08:51:10 +00:00
fix(managed-agents): boot reconcile survives pin-less records
Boot reconcile minted zero 30179s on a store with any record whose
legacy relay_url pin is "". Two defects compounded:
- buzz-core validate_identity_and_config rejected an empty relay_url.
The per-record pin has been ignored at read time since #2122 (the
agent resolves to the active workspace relay), so "" is a legal
record and must encode. Keep only the 4096 upper bound.
- reconcile_agents_in_dir_with used `?` on each record, so the first
build failure aborted the loop and every record after it in file
order was never visited. Log the failing record and keep walking;
each record is its own transaction, so partial progress is safe.
Tests: buzz-core `empty_relay_url_is_accepted`; desktop
`empty_relay_url_pin_does_not_abort_boot_reconcile` writes a pin-less
record followed by a normal one and asserts both get 30177 and a
decryptable 30179. The desktop test fails on f7c247756 with
"invalid relay_url length" and passes with this change.
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -417,7 +417,9 @@ fn validate_identity_and_config(
|
||||
if let Some(auth_tag) = &identity.auth_tag {
|
||||
validate_auth_tag(auth_tag, &owner.to_hex(), agent)?;
|
||||
}
|
||||
if config.relay_url.is_empty() || config.relay_url.len() > 4096 {
|
||||
// Empty is legal: a pin-less agent resolves to the active workspace relay
|
||||
// at read time, so only the upper bound is enforced here.
|
||||
if config.relay_url.len() > 4096 {
|
||||
return Err(Error::InvalidPayload("invalid relay_url length".into()));
|
||||
}
|
||||
if config.name.is_empty() || config.name.len() > 4096 {
|
||||
@@ -764,6 +766,16 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
/// A pin-less agent (empty `relay_url`, legal since the per-record pin
|
||||
/// became read-time-ignored) must still produce a valid payload.
|
||||
#[test]
|
||||
fn empty_relay_url_is_accepted() {
|
||||
let owner = Keys::generate();
|
||||
let mut candidate = payload(&owner, &Keys::generate());
|
||||
candidate.config.relay_url.clear();
|
||||
validate_payload(&candidate).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_owner_attestation_passes_and_binds_agent() {
|
||||
let owner = Keys::generate();
|
||||
|
||||
@@ -144,8 +144,12 @@ fn reconcile_agents_in_dir_with(
|
||||
continue;
|
||||
}
|
||||
|
||||
if retain_agent_record_at_boot(&conn, keys, record)? {
|
||||
reconciled += 1;
|
||||
// One unpublishable record must not strand every record after it in
|
||||
// file order: log it and keep walking.
|
||||
match retain_agent_record_at_boot(&conn, keys, record) {
|
||||
Ok(true) => reconciled += 1,
|
||||
Ok(false) => {}
|
||||
Err(e) => eprintln!("buzz-desktop: agent-event-reconcile: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,51 @@ fn keyring_resident_nsec_is_hydrated_for_private_config() {
|
||||
assert_eq!(payload.identity.private_key_nsec, nsec);
|
||||
}
|
||||
|
||||
/// A record whose legacy `relay_url` pin is empty (legal since #2122 — the
|
||||
/// pin is ignored at read time) must not stop the boot reconcile: the records
|
||||
/// after it in file order still get their first 30179.
|
||||
#[test]
|
||||
fn empty_relay_url_pin_does_not_abort_boot_reconcile() {
|
||||
use nostr::ToBech32;
|
||||
|
||||
let owner_keys = nostr::Keys::generate();
|
||||
let pinless_keys = nostr::Keys::generate();
|
||||
let later_keys = nostr::Keys::generate();
|
||||
let mut pinless = sample_record(&pinless_keys.public_key().to_hex(), "pinless");
|
||||
pinless.relay_url.clear();
|
||||
pinless.private_key_nsec = pinless_keys.secret_key().to_bech32().unwrap();
|
||||
let mut later = sample_record(&later_keys.public_key().to_hex(), "later");
|
||||
later.private_key_nsec = later_keys.secret_key().to_bech32().unwrap();
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
write_store(&dir, &[pinless, later]);
|
||||
let db_path = dir.path().join("retention.db");
|
||||
reconcile_agents_in_dir_with(
|
||||
dir.path(),
|
||||
&owner_keys,
|
||||
&db_path,
|
||||
None::<&crate::secret_store::SecretStore>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let conn = open_retention_db(&db_path).unwrap();
|
||||
let owner = owner_keys.public_key().to_hex();
|
||||
for keys in [&pinless_keys, &later_keys] {
|
||||
let agent = keys.public_key().to_hex();
|
||||
assert!(
|
||||
get_retained_event(&conn, KIND_MANAGED_AGENT, &owner, &agent)
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
"30177 for {agent}"
|
||||
);
|
||||
let row = get_retained_event(&conn, KIND_PRIVATE_MANAGED_AGENT, &owner, &agent)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| panic!("first 30179 for {agent}"));
|
||||
let event = nostr::Event::from_json(&row.raw_event).unwrap();
|
||||
private_managed_agent::validate_and_decrypt(&event, &owner_keys).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_record_is_retained_pending() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user