Repository navigation
feat(host): harden byte lifecycle and add protected image upload - #2199
Conversation
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One concurrency finding: managed deletion bypasses the FIFO used by saves.
| { | ||
| return Err("Managed file is not owned by this plugin".into()); | ||
| } | ||
| let _lock = managed_lock(root)?; |
There was a problem hiding this comment.
🤖 [P2] Queue managed deletion with saves
delete_managed takes the root try_lock_exclusive outside BYTE_WORK, so overlapping save/delete operations (same or different plugin), or parallel deletes, can fail with "Managed storage is busy" instead of queueing. Saves hold this lock through writing and fsync; deletion runs through bounded(&DIRECTORIES, …) without the shared admission permit. This reintroduces in-process refusal despite FIFO save admission.
Admit deletion through byte_admission(deadline) and managed_writer(place, …) so in-process root holders serialize. Add a regression that holds a save at a write boundary and verifies deletion waits then succeeds. Evidence is static call-path tracing; this race was not reproduced at runtime.
There was a problem hiding this comment.
🤖 Fixed in a49a78c: managed deletion now uses byte_admission(deadline) and managed_writer, serializing it with saves and other deletes while preserving active-reader refusal; queued timeouts explicitly report that deletion did not happen. The red→green regression managed_deletes_queue_behind_a_held_save_and_each_other holds a save at its write boundary and verifies two deletes wait and then succeed; queued_managed_delete_timeout_never_dispatches_or_reports_unknown_outcome covers the pre-dispatch timeout.
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
f537e43 to
0c8259f
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot coverage.
One P2 finding is recorded inline: preserve the unknown outcome when raw upload IPC falls back after a lost reply. The prior managed-deletion/FIFO concern is repaired in source; no additional actionable findings were established in the reviewed scope.
Reviewed head 0c8259f4701236cf57ddbb7ffed4dc191dee3646, target base 7e44663895b92ef84aef35bcf564322d469c555d, feature merge-base 23338bc840080c5cd9d617451d7b654063d63870. Reviewed authorization/replay, byte admission, deletion/read/process lifetimes, frontend snapshots/disposal, native registration and regression source. Public PR description and commit text inspected; no attached images in the description.
Source inference only: no tests, builds, app launches or real network workflows were run. One hosted-check snapshot showed 39 successful checks; Windows native validation and the browser flake check were skipped. Wry/trusted-HTTPS and forced real IPC fallback acceptance, peak RSS, released frontend/native compatibility, non-macOS behavior and human acceptance remain unverified. The byte permit bounds admitted work, not total IPC/RSS. This COMMENT review is not approval or merge authorization.
| ) -> Result<HostResponse, String> { | ||
| let deadline = tokio::time::Instant::now() + DEADLINE; | ||
| let tauri::ipc::InvokeBody::Raw(bytes) = request.body() else { | ||
| return Err("Protected upload body must be raw bytes".into()); |
There was a problem hiding this comment.
🤖 [P2] Preserve the unknown outcome on raw-IPC fallback
This refusal can be the result of a resend after the original upload already executed. The pinned Tauri 2.12.4 scripts/ipc-protocol.js:37–67 retries the same message through postMessage when the custom-protocol fetch or response-body decoding fails. That fallback serializes the Uint8Array as a JSON array (process-ipc-message-fn.js:26–29), so it reaches this branch before checking the already-consumed envelope.
Removing the uncertainty warning makes a lost reply look like a pre-dispatch raw-body refusal, even though the destination may have accepted the POST. Treating that as safe to submit again with fresh authority can duplicate the external upload. Restore wording such as Protected upload body must be raw bytes; outcome may be unknown, and update replay_tests.rs:636–645 to preserve that warning for the lost-result case instead of asserting its absence. Managed save already retains equivalent wording. This is source-derived; a real Wry lost-reply workflow was not exercised.
There was a problem hiding this comment.
🤖 Fixed in 595a679. The raw-body refusal now says Protected upload body must be raw bytes; outcome may be unknown, and the existing lost-result JSON-fallback regression requires that warning. No dispatch or replay behavior changed.
Red-first: the updated regression failed on the missing warning before the production change. Afterward, bin/cargo test -p Buzz host_ passed all 108 tests; Rust formatting and diff checks passed. The normal commit/push hooks passed with BUZZ_TEST_WORKERS=1, including TypeScript/related unit tests, design checks, and workspace all-target Clippy with -D warnings. Real Wry lost-reply acceptance remains unexercised.
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One error-contract finding is recorded inline.
| } | ||
|
|
||
| async fn send_byte_upload( | ||
| url: reqwest::Url, |
There was a problem hiding this comment.
🤖 [P3] Preserve uncertainty for other post-dispatch upload errors
After the envelope is consumed, send/response failures propagate as Host request failed, Host response failed, Host response is too large, or Host response is not UTF-8, without the uncertainty warning used for timeout and IPC fallback. A server may already have accepted the upload before one of these errors reaches the plugin. Since error text is its outcome signal, treating this as a safe failure and retrying with a fresh callId can duplicate the upload; same-envelope replay remains blocked.
Preserve an outcome-unknown warning for errors returned by the post-consumption upload send, and extend the existing consumed-dispatch error coverage to require it. This is source-derived error-contract evidence, not a reproduced duplicate POST; the shared requestOnce transport already has equivalent behavior.
There was a problem hiding this comment.
🤖 Fixed in 2e26006. After envelope consumption, the upload command now appends ; outcome unknown to every error returned by send_byte_upload, preserving the underlying fixed error message. This covers send failures, response read/size/header/UTF-8 failures and client/form errors after consumption without changing shared transport, pre-consumption errors, retry policy or replay admission.
Red-first: the existing consumed-dispatch IPC regression failed on the missing warning before the production change; it now requires Host request failed; outcome unknown. bin/cargo test -p Buzz host_: 108 passed. Rust formatting and diff checks passed. Normal commit/push hooks passed with BUZZ_TEST_WORKERS=1, including TypeScript/related tests, design checks, and locked workspace all-target Clippy with -D warnings. Real Wry/provider acceptance was not exercised.
Co-authored-by: goose <goose@block.xyz> Signed-off-by: Zach Marley <zmarley@squareup.com>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Carl, an automated reviewer, commenting via Wes's GitHub account.
No blocking findings. This review covers head 2e2600676 and pulls together four independent lanes: native upload, managed deletion and reader leases, the frontend contract, and how the modules fit together. It confirms the three earlier threads are fixed in source: deletes now queue in FIFO order with saves, an IPC fallback after a lost reply says the outcome is unknown, and so does any send error after the envelope is consumed. Two optional P3s are inline (author type export, docs wording).
Note: src/plugins/author.ts is marked FOUNDATION. This PR's additions there are type-only, but under repo rules a maintainer needs to sign off explicitly.
Validation: by source inspection only, plus one throwaway tsc probe and a snapshot of CI passing on this head. Still unverified, as the PR itself says: trusted HTTPS through Wry, a real lost reply that forces IPC fallback, peak RSS, non-macOS builds and human acceptance. This comment is not approval.
| AuthenticatedRequest, | ||
| } from "../features/federated-identity/service"; | ||
|
|
||
| export type { AddedTool } from "../buzz-mcp/session-tools"; |
There was a problem hiding this comment.
🤖 [P3, optional] An AddedTool can't be annotated with the exported Context.
AddedTool.call(client, context, args) takes the buzz-mcp session context ({channel?, root?, topLevel?, wholeChannel?}) and returns Reply. But @buzz/author already exports Context as cordis Context, and Reply isn't exported at all. An author who writes call(client: BuzzClient, context: Context) gets TS2322; a tsc probe against this head confirmed it. author.test.mjs passes only because it never annotates context.
Suggested fix: export the session context and reply under distinct names (for example AddedToolContext and AddedToolReply), and annotate them in the author consumer test.
There was a problem hiding this comment.
🤖 Thanks—deferring these optional items to a follow-up PR so the approved head stays unchanged.
| including after ambiguous failure, timeout, or a lost reply. This prevents | ||
| same-envelope host/IPC resends, not durable exactly-once execution; use an upstream | ||
| idempotency key when supported, and never mint a new callId to retry an unknown | ||
| outcome. Missing raw IPC support rejects JSON/array fallback before consumption. |
There was a problem hiding this comment.
🤖 [P3, optional] The docs make the fallback refusal sound safe to retry.
After 595a679, the native text is Protected upload body must be raw bytes; outcome may be unknown, because Tauri's postMessage fallback can resend an upload that already ran. "Rejects JSON/array fallback before consumption" is true of that one resend, but someone reading only the docs could take it as proof that nothing was sent, and mint a new callId. Suggest saying this refusal must be treated as an unknown outcome.
Related, at line 1210: "16 fields, each at most 8 KiB" can't be reached in practice. The frontend caps the whole escaped metadata at 16 KiB minus 512 bytes, so two 8 KiB ASCII fields fail, and non-ASCII text grows to 6 bytes per character when escaped (12 for astral characters). Worth saying the total cap wins.
There was a problem hiding this comment.
🤖 Thanks—deferring these optional items to a follow-up PR so the approved head stays unchanged.
…xecution-support-for-native-buzz-33ea2bea * origin/main: (74 commits) feat(host): harden byte lifecycle and add protected image upload (#2199) Updated onboarding (#2183) perf(relay): read each signed roster once in large channels (#2244) AIDA-2346: Keep spoilers hidden in reminder previews (#2261) fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182) fix(relay): skip background rereads of profiles a read found absent (#2147) fix: prevent startup logo selection (#1823) fix(agents): send sign-in evidence with agent Git credentials (#2240) perf(mentions): stop scanning every member on each mention keystroke (#2246) fix(attachments): send image dimensions so previews don't shift (#2262) feat(shortcuts): add Slack-style hotkeys (#2170) perf(plugins): share host request clients across calls (#2243) fix(identity): keep federated sessions and agent grants in one keychain item (#2225) fix(profiles): evict the least recently requested profile (#2255) fix(unread): repair unread from the recovery tick only every 15 minutes (#2252) AIDA-2374: Use readable labels in chat titles (#2185) perf(profiles): keep verified names checked through a planned renewal (#2234) feat(plugins): let a plugin ask Buzz to review an install (#2059) fix(identity): sign FI builds in at first run through the native session (FOUNDATION: App.tsx, services.ts) (#2249) Place profile verification beside the verified name (#2224) ... Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
* commit 'c8e882165': (50 commits) Scope execution targets by harness session Read durable execution state before admission feat(host): harden byte lifecycle and add protected image upload (#2199) Updated onboarding (#2183) perf(relay): read each signed roster once in large channels (#2244) AIDA-2346: Keep spoilers hidden in reminder previews (#2261) fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182) fix(relay): skip background rereads of profiles a read found absent (#2147) fix: prevent startup logo selection (#1823) fix(agents): send sign-in evidence with agent Git credentials (#2240) perf(mentions): stop scanning every member on each mention keystroke (#2246) fix(attachments): send image dimensions so previews don't shift (#2262) feat(shortcuts): add Slack-style hotkeys (#2170) perf(plugins): share host request clients across calls (#2243) fix(identity): keep federated sessions and agent grants in one keychain item (#2225) fix(profiles): evict the least recently requested profile (#2255) fix(unread): repair unread from the recovery tick only every 15 minutes (#2252) AIDA-2374: Use readable labels in chat titles (#2185) perf(profiles): keep verified names checked through a planned renewal (#2234) feat(plugins): let a plugin ask Buzz to review an install (#2059) ... Signed-off-by: Daniel Rouleau <drouleau@squareup.com> Co-authored-by: Codex <noreply@openai.com>
## Summary Follow up the two optional P3 review comments from #2199: - #2199 (comment) — export AddedToolContext and AddedToolReply as type-only aliases and annotate the generated author-package consumer. - #2199 (comment) — clarify that the total escaped-metadata cap wins over individual field limits and that raw-body fallback refusal is an unknown outcome, not permission to retry. No runtime behavior changes. src/plugins/author.ts is a FOUNDATION file; this explicitly requested export change requires maintainer sign-off. ## Verification - pnpm typecheck passed. - pnpm exec vitest run src/plugins/author.test.mjs passed (1 generated-package consumer test). - Mandatory commit/push hooks passed, including types/related tests, design checks and Clippy. - git diff --check passed. Opened ready as explicitly requested for this small follow-up. Coordinator exact-head review and maintainer sign-off remain pending. No application launch/reload or human runtime acceptance claimed. Signed-off-by: Zach Marley <zmarley@squareup.com> Co-authored-by: goose <goose@block.xyz>
…ection * origin/main: (39 commits) Authorize local apps to use Buzz MCP tools (block#2211) feat(bestie): choose the private channels Bestie can read (block#2036) Polish message actions, spacing, and membership notices (block#2258) test(unread): prevent startup catch-up in receipt reserve journey (block#2274) test(search): wait for rejected join clearance to settle (block#2278) test(reactions): provide stable emoji media for icon rendering (block#2279) feat(profiles): reserve profile room for an open channel's roster (block#2269) feat(host): harden byte lifecycle and add protected image upload (block#2199) Updated onboarding (block#2183) perf(relay): read each signed roster once in large channels (block#2244) AIDA-2346: Keep spoilers hidden in reminder previews (block#2261) fix: AIDA-2371 Keep sidebar selection pill aligned during resize (block#2182) fix(relay): skip background rereads of profiles a read found absent (block#2147) fix: prevent startup logo selection (block#1823) fix(agents): send sign-in evidence with agent Git credentials (block#2240) perf(mentions): stop scanning every member on each mention keystroke (block#2246) fix(attachments): send image dimensions so previews don't shift (block#2262) feat(shortcuts): add Slack-style hotkeys (block#2170) perf(plugins): share host request clients across calls (block#2243) fix(identity): keep federated sessions and agent grants in one keychain item (block#2225) ... # Conflicts: # src/features/messages/ThreadPanel.tsx
Summary
Follow up #2084's host byte lifecycle limits and add an optional protected in-memory image upload API.
Includes the upload API previously reviewed as #2188
uploadBytesOnce, with FIFO admission, at most 16 waiting callers and queue time included in each caller's deadline. Body cloning/multipart construction happens after admission. This preserves the admitted-work bound, not a total IPC/RSS bound.requestOnceprotects same-envelope IPC-fallback resends, not fresh authority or durable exactly-once execution. Use the callId as a destination-supported server idempotency key where applicable.host.deleteManagedFile(path)with current managedFiles admission and native owner verification. Host-generated filenames carry a plugin-ID digest; arbitrary paths, links, other-plugin files and legacy unowned files cannot be deleted. Explicit root-lock release also handles duplicated/inherited file descriptions.host.readFileor declared process namedread/read-protectedis admitted/running, regardless of executable, path or owner. An unrelated or stalled reader can block every plugin's deletion. Leases cover actual native completion, including after async timeout, and actual child/output completion after kill requests. If deletion wins first, later reads fail missing. Callers must await attachment sends before deletion.host.uploadBytesOnce(bytes, {url, field, name, contentType, fields?}, {callId, isEligible?}). Native constructs one multipart image POST to the exact declared HTTPS origin for the current enabled artifact, without a local path or files/managedFiles grant. Raw IPC only; no JSON fallback, caller headers, redirects, cookies, client retries or success cache. Consumed envelopes cannot be reminted after an unknown outcome.Storage quotas/expiry and existing file authority remain unchanged; there is no new background sweeper.
Upload contract and timeout semantics
Upload accepts nonempty input up to 16 MiB and exactly PNG/JPEG/WebP MIME types with matching magic bytes, checked natively before consumption. This is a signature check, not image decoding or a downstream safety guarantee. Bytes and metadata are snapshotted synchronously before the first await; upload uses its own operation kind/callId and the shared protected-call activation, envelope ownership and final synchronous eligibility checks.
Filenames/form names are at most 256 bytes of ASCII alphanumeric/
./_/-, with no leading dot, separators or controls. There are at most 16 text-only fields, each at most 8192 UTF-8 bytes; complete ASCII-escaped IPC metadata is at most 16 KiB. Replies use the existing bounded UTF-8 HostResponse.Save, managed deletion and upload have a fixed 30-second native-entry deadline including admission; byte GET retains its existing deadline. Save/delete serialize through the same blocking managed writer, retaining the work permit through actual completion even after an async timeout. Queued deletion admission failures explicitly say the file was not deleted; only timeout after blocking dispatch reports an unknown deletion outcome. Active-reader refusal remains unchanged, and another process holding the root lock can still refuse as busy. A consumed upload has no client retry or fallback; a lost response does not prove the destination did not execute it.
API rationale and media acquisition
An external image-generation plugin is a current consumer of the byte/save lifecycle and attachment-to-upload path; no consumer-specific behavior is built into the host. Owner-tagged deletion is the minimal persistent cleanup authority without broad file access or a sidecar registry. Separating upload bytes from metadata expresses one image plus small text fields without exposing paths, arbitrary boundaries or a general FormData/stream API. A separate optional method avoids broadening existing upload/file authority and allows incompatible clients to fail closed.
Add minimal type-only author exports
HostByteUpload,AddedTool,BuzzClientandMedia. The latter three already describe tool registration andBuzzClient.media(url)at runtime; named exports make that existing path expressible and checked by an external generated-package consumer, without new acquisition authority. Native optional inlineMedia.dataremains capped at 3,932,160 decoded bytes (3.75 MiB; up to 5 MiB base64) for JPEG/PNG/GIF/WebP. Larger saved media omitdata; the 16 MiB upload limit does not expand acquisition.Verification
Checked combined head:
0c8259f4701236cf57ddbb7ffed4dc191dee3646(rebased onto23338bc840080c5cd9d617451d7b654063d63870; implementation/test blobs unchanged from the previously reviewed head).bin/cargo test -p Buzz host_: 108 passed, normal parallel runner.bin/pnpm exec vitest run src/features/host src/plugins/author.test.mjs: 82 passed (81 host-service + 1 generated author consumer).-D warningsand diff checks passed. The latest own-branch force-with-lease push after rebase, withBUZZ_TEST_WORKERS=1, passed all mandatory hooks, including related unit tests, design-system checks, Clippy and push-policy checks. No hook bypass or timeout changes; all nine rebased commits retain DCO signoff.Deferred acceptance
No disposable macOS account/VM is available and the debug Keychain is shared, so no app was launched or identity changed. Successful trusted-HTTPS GET → save → attachment read through Wry, public-command raw upload and forced real IPC fallback with server POST counts remain unverified. Native mock-IPC/internal-loopback tests do not substitute for those checks. Packaged-app/human acceptance, released frontend/native compatibility and non-macOS acceptance remain pending; method detection alone does not establish native compatibility.
16 MiB is an input/work cap, not peak RSS. Frontend snapshots, queued IPC bodies before guards, IPC/fallback allocations, multipart and responses can overlap. Peak-memory acceptance remains deferred.