Skip to content

fix: reject empty stream messages at CLI, SDK, and relay ingest - #5394

Open
Cynthia427 wants to merge 1 commit into
block:mainfrom
Cynthia427:fix/reject-empty-stream-message
Open

Cynthia427 wants to merge 1 commit into
block:mainfrom
Cynthia427:fix/reject-empty-stream-message

Conversation

@Cynthia427

Copy link
Copy Markdown

What

A production agent's channel post was published with empty content: its oversized turn output was rejected by the 64 KiB cap, the retry piped content via stdin, the pipe delivered zero bytes, and "" passed every gate — CLI validation, SDK builder, and relay ingest all accept empty content for kind 9. The author (an automated agent) believed the message was posted; the recipients saw a blank bubble. Silent content loss.

Fix — three layers, innermost out

  • buzz-cli cmd_send_message: fail fast with a usage error that names the stdin-pipe case when content is empty/whitespace-only and no --files are attached.
  • buzz-sdk build_message: new SdkError::EmptyContent unless media tags are present — every kind-9 producer inherits the guard.
  • buzz-relay ingest: reject kind 9 with empty content and no imeta tags, covering non-CLI clients.

Deliberately unaffected

  • Media-only messages (imeta tags, no text) remain legal at every layer.
  • Message edits (kind 40003): clearing an edit to empty is the deletion gesture (feat(desktop): delete a message by clearing its edit to empty #3813) and flows through a different kind — untouched.
  • mem set <slug> '' and other non-kind-9 empty-content uses — untouched (the guards are kind-9-scoped).

Tests

Three new SDK unit tests (empty rejected, whitespace-only rejected, media-only allowed); cargo test -p buzz-sdk green (187), buzz-cli + buzz-relay compile clean.

🤖 Generated with Claude Code

An agent's channel post was published with empty content after its
oversized turn output was rejected and a stdin retry delivered zero
bytes — every gate accepted "" and the author believed the message
was posted (silent content loss, discovered in production).

Three layers, innermost out:
- buzz-cli cmd_send_message: fail fast with a usage error naming the
  stdin-pipe case when content is empty/whitespace-only and no --files
  are attached
- buzz-sdk build_message: SdkError::EmptyContent unless media tags are
  present, so every kind-9 producer inherits the guard
- buzz-relay ingest: reject kind 9 with empty content and no imeta
  tags, covering non-CLI clients

Deliberately unaffected: media-only messages (imeta, no text) stay
legal, and message edits (kind 40003) are untouched — clearing an edit
to empty is the deletion gesture (block#3813) and flows through a different
kind.

Signed-off-by: Cynthia Rohr <cynthia.r@kreativreason.co>
@nathansmithopenclaw-alt

Copy link
Copy Markdown

An empty variable or an empty stdin producer can publish a blank message successfully today. I prepared a complete fix that builds on the three-layer approach here and the CLI-first work in #7050.

The contribution is one signed-off commit on upstream main 3c7f288c60d67df78577b237e27c3dfc8831aaa1: 4c1e63d32, on codex/item16-upstream-complete-20260907. It is ready to cherry-pick or adapt into this PR.

The combined behavior is:

  • CLI: reject blank literal/stdin input before network access, with the actual captionless-file syntax in the error and help: --content "" --file PATH. Failed uploads still abort publication.
  • SDK: reject blank text in the stream-message, forum-post and forum-comment builders unless an imeta tag contains a usable HTTP(S) attachment URL. This reuses SdkError::InvalidInput.
  • Relay: enforce the rule for stream messages (9 and legacy 40002), forum posts (45001) and forum comments (45003), after existing media metadata, stored-blob and tenant checks, in shared HTTP/WebSocket ingest. Arbitrary tags or nonexistent media cannot bypass it.

Validation does not trim or normalize content. Captionless attachments, Markdown/emoji, reply ancestry and edits that clear text remain supported. Stdin reads through EOF without a first-byte timeout. The payload contract also explains why partial nonempty stdout cannot prove that its producer succeeded, and gives a staged-report recipe.

Local verification on that exact commit:

  • Four real-CLI process tests against a recording HTTP fixture, including a producer delayed 5.25 seconds; five public SDK builder tests.
  • One authenticated HTTP/WebSocket test with 49 publication cases and database readback, using real PostgreSQL, Redis and S3 blobs. It covers whitespace, preserved text, media-only payloads, malformed/missing media and empty edits.
  • On the fork implementation, five production-guard removal controls: removing each SDK call site or the CLI/relay guard causes its rejection assertions to fail; separate positive controls pass. The regression files and SDK/CLI guard helpers are byte-identical between the fork and this upstream cut.
  • Complete SDK and CLI libraries: 306 and 463 tests pass. just test-unit passes. Desktop JavaScript (6,450), native desktop main library (3,165; 19 ignored) and mobile (2,066) tests pass, along with format/lint/static checks and desktop/web builds. The native workspace/doctest stage passed on retry after a compiled-dependency resolution failure, without source changes. The original just ci aggregate exit was nonzero; all its stages passed across the initial run and retry.
  • just test remains nonzero: 16 groups passed and two failed. The DB source-observability assertion and relay demo-join HTTP 504 were reproduced on the unchanged upstream base. An agent cancellation test failed once but passed isolated on candidate and base; that result remains classified as transient. These are recorded exceptions, not an all-green integration claim.

The new tests are wired into the unit and Relay E2E CI jobs. The same regression contract passes on our fork. These are local test results; no hosted CI, deployed behavior or upstream approval is claimed. A final relay rerun initially stopped at PostgreSQL connection setup because the disposable harness had been removed; it passed against a recreated harness without source changes.

To rerun the focused contract after configuring disposable PostgreSQL, Redis and S3:

cargo test -p buzz-cli -p buzz-sdk --test message_payload
cargo test -p buzz-relay --test message_payload -- --ignored --nocapture

I suggest consolidating this into the existing PR and coordinating #7050's overlapping CLI work. This supplies the broader enforcement, regression tests and protocol documentation together without opening another overlapping proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants