[3/4] buzz personas: publish agent definitions from the CLI - #5844
[3/4] buzz personas: publish agent definitions from the CLI#5844Maxwellimus wants to merge 1 commit into
Conversation
wolfyy970
left a comment
There was a problem hiding this comment.
The owner-signing boundary and the monotonic create/delete handling look right, and the focused suites pass 35/35. I found two paths that still let the CLI report success for a definition Desktop cannot instantiate.
-
The behavioral validation is incomplete.
--parallelismaccepts 0 or values above 32, while--fromcasts any JSONu64tou32—so4294967297silently becomes 1. Imported allowlists are checked only for being non-empty; the current test even blesses"aa"and"bb", although Desktop requires normalized, deduplicated 64-character hex pubkeys. Please run both flags and imported values through the same mint-time validation before publishing, with regressions for 0, 33, overflow, malformed keys, and uppercase duplicates. -
--fromis not yet a fail-closed Desktop snapshot decode. It reads an unbounded file into a looseValue, thenfilter_mapsilently drops wrong-typedrespondToAllowlistandnamePoolentries and accepts incomplete v1 envelopes. That repairs malformed input under the user, contrary to the boundary described in the PR. Please cap JSON at the Desktop 5 MiB limit and decode and validate a typed v1 snapshot shape before projecting the deliberately portable definition fields.
These are narrow fixes; I would keep the current 30175 publication architecture.
976da4f to
bf0a60c
Compare
cf20d8a to
3ba9ecb
Compare
3ba9ecb to
cbd821f
Compare
Personas could only be created by clicking through Buzz Desktop, so nothing scriptable could stand up an agent roster — including the agents themselves. `buzz personas create|list|get|delete` writes the same kind:30175 coordinates Desktop reads, from a flag set or straight from a `.agent.json` export. These are owner-authored events, so the signing key IS the owner: no NIP-OA auth tag is involved, and running with a different key publishes to a coordinate space the owner's Desktop never reads. Publishing a definition does not start an agent; launching one mints key material and stays a Desktop operation. Two relay behaviors shape the implementation: - A write must be stamped past the coordinate's current head. NIP-33 breaks a `created_at` tie by lowest event id, so a same-second rewrite can otherwise lose to the event it was replacing. - `soft_delete_by_coordinate` matches `created_at <= tombstone`, and its result only feeds a debug log. A tombstone older than its target is accepted, deletes nothing, and reports OK — so delete stamps from the head it just read and then re-reads the coordinate to confirm. The re-read only raises a conflict for a head strictly newer than the tombstone, since a lagging replica can still return the deleted head. `--from` refuses rather than repairs a snapshot that would publish a persona Desktop can't mint from: `respondTo=allowlist` with no pubkeys, an unknown `respondTo`, or definition text carrying invisible characters. Avatars follow Desktop's reader rather than a CLI-only rule. `--avatar` takes a local image and carries it inline as a data URL when it fits the bounds Desktop renders — 8 KiB for SVG, 256 KiB for raster, both inside the relay's 256 KiB content cap. Anything larger is downscaled to 512px and re-encoded, then re-checked against the bound: flat art typically lands back inside it and never reaches media storage at all, and only what still doesn't fit is uploaded. Re-encoding is also what makes that upload work. Media storage refuses images carrying metadata — EXIF, colour profiles, comments — as an identity channel, so a photo straight off a camera failed outright. Decoding and re-encoding drops metadata by construction rather than by stripping known chunks, so it cannot drift from the relay's allowlist the way a structural stripper would. EXIF orientation is baked into the pixels first, because dropping the tag without applying it publishes the avatar sideways. GIF passes through untouched, since re-encoding would flatten animation, and WebP re-encodes to PNG because `image`'s WebP encoder is lossless-only and would inflate a lossy source. `--from` carries a snapshot's inlined avatar through that same path, so a Desktop export round-trips with its image. The upload runs after the `--replace` conflict check: an upload that a rejected write would strand leaves an orphan blob behind. Signed-off-by: Max Lampert <maxwell@squareup.com>
cbd821f to
594e2ec
Compare
|
🤖 Both items addressed in Behavioral validation. The u32 truncation is gone rather than range-checked: Fail-closed snapshot decode. I extended the same treatment to teams in [4/4], which has the identical defect One thing beyond your review, since it is the same class of silent-repair Agreed on keeping the 30175 publication architecture. |
wolfyy970
left a comment
There was a problem hiding this comment.
Thanks, Max. I reviewed 594e2ece9. The two items from 976da4f74e21 are fixed: parallelism is bounded on both paths, overflow no longer truncates, allowlists match Desktop mint rules, and --from fails closed on a wrong-typed v1 definition instead of repairing it.
One new hole in the coordinate-adoption work:
--slug is still overwritten when --from finds an identical published persona. resolve_create says flags win, and they do until cmd_create replaces the slug. buzz personas create --from herring.agent.json --slug herring-copy --replace then overwrites the UUID coordinate Desktop already published, and never writes herring-copy. Skip adoption when --slug is set. A caller who named a coordinate should get that coordinate, or a conflict at that coordinate.
Keep the adoption for the no-slug --from case. That is the right fix for the duplicate Desktop UUID.
Personas could only be created by clicking through Buzz Desktop, so nothing
scriptable could stand up an agent roster — including the agents themselves.
buzz personas create|list|get|deletewrites the same kind:30175 coordinatesDesktop reads, from a flag set or straight from a
.agent.jsonexport.These are owner-authored events, so the signing key is the owner: no NIP-OA
auth tag is involved, and running with a different key publishes to a coordinate
space the owner's Desktop never reads. Publishing a definition does not start an
agent — launching one mints key material and stays a Desktop operation.
Two relay behaviors shape the implementation
created_attie by lowest event id, so a same-second rewrite can otherwiselose to the event it was replacing.
soft_delete_by_coordinatematchescreated_at <= tombstone, and its resultonly feeds a debug log. A tombstone older than its target is accepted, deletes
nothing, and reports OK — so delete stamps from the head it just read, then
re-reads the coordinate to confirm. The re-read only raises a conflict for a
head strictly newer than the tombstone, since a lagging replica can still
return the deleted head.
--replacecarries the publishedsharedtag forward. A snapshot has nosharedfield and the flag defaults off, so re-publishing a shared persona to fixa typo would otherwise drop it out of every other member's catalog silently;
personas getnow prints the state so it is checkable.Snapshots and avatars
--fromdecodes a typed v1 snapshot rather than probing a loose JSON value, so awrong-typed field is an error instead of a silently dropped entry, and the file is
capped at the 5 MiB Desktop itself refuses to import. It refuses rather than
repairs a snapshot that would publish a persona Desktop can't mint from:
respondTo=allowlistwith no pubkeys, an allowlist entry that isn't 64 hexchars,
parallelismoutside 1-32, an unknownrespondTo, or definition textcarrying invisible characters.
--parallelismis bounded at the flag by the same1-32 range.
A snapshot carries no id, so the slug derived from its name would mint a second
coordinate for a persona Desktop already publishes under its record UUID, leaving
two records of the same name. An import adopts the published coordinate — but
only when the definition there is the one being imported. A shared display name
alone can be a different persona, and adopting its coordinate would overwrite it;
that case publishes at its own slug and says so on stderr. The comparison covers
every field the snapshot's
definitionblock carries and deliberately excludesthe avatar, which lives in
profileand is re-encoded on the way in.Avatars follow Desktop's reader rather than a CLI-only rule.
--avatartakes alocal image, downscales it to 512px, re-encodes it, and then measures: it rides
inline in the event when it fits the bounds Desktop renders — 8 KiB for SVG,
256 KiB for raster, both inside the relay's 256 KiB content cap — and uploads to
media storage when it doesn't. Flat art typically lands back inside the bound and
never reaches media storage at all.
--avatar-urlis checked against the samerules Desktop's reader applies, so a URL it would silently drop is refused
instead of published to render as nothing.
Re-encoding, not the size check, is the load-bearing part. Metadata — EXIF,
colour profiles, comments — is an identity channel: media storage refuses it
outright, so a photo straight off a camera failed to upload, and an inline avatar
bypasses that validator entirely and would carry the photo's GPS coordinates into
a world-readable event. Every raster is re-encoded for that reason, whichever
path it takes. Decoding and re-encoding drops metadata by construction rather
than by stripping known chunks, so it cannot drift from the relay's allowlist the
way a structural stripper would. EXIF orientation is baked into the pixels first,
because dropping the tag without applying it publishes the avatar sideways. The
decoder is bounded to 8192px a side, so a small file declaring huge dimensions is
refused rather than expanded. GIF and animated images pass through whole, since
re-encoding would flatten them to one frame, and WebP re-encodes to PNG because
image's WebP encoder is lossless-only and would inflate a lossy source.--fromcarries a snapshot's inlined avatar through that same path, so aDesktop export round-trips with its image. The upload runs after the
--replaceconflict check: an upload that a rejected write would strand leaves an orphan
blob behind.
Test plan
just ciLive against a local relay:
(2.2× the bound) publishes inline at 7,458 chars, no upload
is lossless and photo detail stays past the bound
tEXtchunk previouslyfailed with
422 media contains metadata; now succeeds--avatar+--avatar-url→exit 1
The inline metadata-strip and the URL/allowlist/parallelism gates landed after
that live run and are covered by unit tests only; the runbook rows for them are
in §6.13.
Runbook rows added to
crates/buzz-cli/TESTING.md§6.13.Deferred
WebP pass through untouched rather than being flattened to a first frame, so
they are the one avatar kind that can still carry metadata: past the inline
bound they hit the media validator and fail with an actionable message, and
inside it they publish with their metadata intact. Desktop strips these
structurally in
media_gif.rsandmedia_animated.rs(~930 lines); movingthose to a shared crate is the follow-up, and it also closes the identical
pre-existing gap in
buzz upload file, which has never sanitized its uploadseither.
normalize_avatarduplicates a ~20-line decode/orient/encode sequence thatDesktop's
sanitize_image_for_uploadalso performs. Depending onbuzz-mediainstead would pull
rust-s3,axum, andmp4into this binary, and it has nofeatures to gate that. The duplication is a mechanism, not a policy —
re-encoding yields metadata-free output by construction, so there is no
allowlist to keep in sync with the relay's validator.
list_owned's 500-event cap. It warns on stderr when ithits the cap rather than truncating silently.
--formatflag isn't threaded through these subcommands, matching19 of the 24 existing dispatch arms.