feat(reborn): accept inline attachment uploads on the WebChat v2 send path (#4644) - #4672
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
a6b08c7 to
e67f7e8
Compare
aedabef to
b10f577
Compare
📝 WalkthroughWalkthroughAdds end-to-end inbound attachment support for the WebUI v2 ChangesWebUI Inbound Attachment Landing
Sequence Diagram(s)sequenceDiagram
participant WebUI as WebUI Handler
participant RS as RebornServices.submit_turn
participant Lander as InboundAttachmentLander
participant FS as ScopedFilesystem
participant Thread as accept_inbound_message
WebUI->>RS: WebUiSendMessageRequest (base64 attachments)
RS->>RS: decode_attachments() → Vec<InboundAttachment>
alt attachments present, lander wired
RS->>Lander: land(thread_scope, message_id, attachments)
Lander->>FS: land_inbound_attachments(date-partitioned path, max_bytes)
FS-->>Lander: Vec<AttachmentRef>
Lander-->>RS: Vec<AttachmentRef>
RS->>RS: MessageContent with attachment refs
else attachments present, no lander wired
RS-->>WebUI: 503 ServiceUnavailable
else no attachments
RS->>RS: text-only MessageContent
end
RS->>Thread: accept_inbound_message(message_content)
Thread-->>WebUI: turn accepted
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
af3acc6 to
8b005f8
Compare
… path (#4644) End-to-end ingress wiring for #4644: a browser can now attach files to a WebChat v2 message, the bytes land in project storage, and the user message persists attachment references. This is the call site for the land_inbound_attachments bridge. Flow (no src/ changes): - DTO: WebUiSendMessageRequest gains `attachments: Vec<WebUiInboundAttachment>` (mime_type, filename, base64). decode_attachments() validates MIME against the shared format registry (Track 1), decodes base64, and enforces the v1 budgets (5 MiB/file, 10 MiB total, 10 files max). Kept separate from into_command so the serializable command never carries raw bytes. - Facade: RebornServices::submit_turn decodes attachments, lands them through a new InboundAttachmentLander port (using the stable per-message external_event_id for the storage path), and builds MessageContent::with_attachments before accept_inbound_message. With no lander wired, an attachment-bearing message is rejected (503) rather than silently dropped. - Port impl: composition's ProjectScopedAttachmentLander writes through the project-scoped workspace ScopedFilesystem (the same authority the agent's file tools resolve through) via land_inbound_attachments, and is wired onto the facade in build_webui_services when a local runtime is present. - Body limit: the send_message route descriptor goes from 1 MiB to 14 MiB to carry base64 of the 10 MiB decoded cap; the descriptor/body-limit contract tests and the composition CLAUDE.md are updated to match. Tests: - decode_attachments: metadata/kind/bytes, MIME normalization, unsupported MIME, malformed base64, per-file/total oversize, too-many, empty. - ProjectScopedAttachmentLander: lands + returns ref with storage_key; read-only workspace mount maps to an internal error. - Facade (test through the caller): submit_turn lands the attachment and the accepted user message carries the ref with storage_key; attachments without a wired lander are rejected with ServiceUnavailable. New dep edges product_workflow/composition -> ironclaw_attachments (accepted by the reborn dependency-boundary test). Deferred: model-visibility (content_parts / extracted_text / project_path), the memory index note, and the static frontend "+ button" upload UI.
…onstructors Reuse the error type's own constructors instead of open-coding status/kind: - submit_turn's no-lander path now calls RebornServicesError::service_unavailable(false) instead of an inline from_status_kind(Unavailable, ServiceUnavailable, 503, false). - Add a public RebornServicesError::internal() so host-composition adapters stop hand-rolling the full struct literal (which silently drifts if a field is added); ProjectScopedAttachmentLander uses it. - The lander no longer discards the underlying AttachmentLandingError: it logs it (warn) before mapping to the sanitized 500, so an operator can tell a misconfigured read-only mount from a write failure. - decode_attachments derives the per-attachment id from enumerate() rather than the decoded-accumulator length.
land_inbound_attachments gained a required max_bytes bound (Track 6/#4670). The ProjectScopedAttachmentLander now owns that policy — set to DEFAULT_MAX_ATTACHMENT_BYTES — and passes it per landing. The send_message route's 14 MiB body cap is the primary gate; this is defense in depth so a single attachment can never land unbounded bytes.
b10f577 to
3da460d
Compare
There was a problem hiding this comment.
Pull request overview
Wires WebChat v2 inline attachment uploads through the Reborn WebUI v2 send-message path so uploaded bytes are decoded, budget-checked, landed into project storage via the workspace filesystem authority, and persisted as transcript attachment references.
Changes:
- Added
attachmentssupport toWebUiSendMessageRequest, including base64 decoding + MIME validation + decoded-byte budgeting. - Introduced an
InboundAttachmentLanderport inironclaw_product_workflowand a composition implementation that lands bytes via the project-scopedScopedFilesystem. - Increased WebUI v2
send_messagerequest body limit to 14 MiB and updated descriptor/body-limit contract tests + docs accordingly.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs | Updates contract test to expect 14 MiB send_message body limit. |
| crates/ironclaw_webui_v2/src/descriptors.rs | Raises send_message descriptor body limit to 14 MiB with updated rationale. |
| crates/ironclaw_reborn_composition/src/webui.rs | Wires the inbound attachment lander into the WebUI services when a local runtime filesystem is available. |
| crates/ironclaw_reborn_composition/src/webui_body_limit.rs | Updates enforcement docs/tests for the new 14 MiB descriptor limit. |
| crates/ironclaw_reborn_composition/src/runtime.rs | Exposes the workspace scoped filesystem for WebUI attachment landing; updates test request DTOs to include empty attachments. |
| crates/ironclaw_reborn_composition/src/local_dev_mounts.rs | Makes WORKSPACE_ALIAS visible within the crate for reuse by attachment landing. |
| crates/ironclaw_reborn_composition/src/lib.rs | Registers the new attachment_landing module. |
| crates/ironclaw_reborn_composition/src/attachment_landing.rs | Implements InboundAttachmentLander using land_inbound_attachments via the workspace ScopedFilesystem. |
| crates/ironclaw_reborn_composition/CLAUDE.md | Updates composition documentation to reflect the 14 MiB send_message body limit. |
| crates/ironclaw_reborn_composition/Cargo.toml | Adds dependency on ironclaw_attachments for landing support. |
| crates/ironclaw_product_workflow/tests/webui_inbound_contract.rs | Adds decode/budget/MIME/base64 tests for inline attachments. |
| crates/ironclaw_product_workflow/tests/reborn_services_contract.rs | Adds facade contract tests covering landing + transcript persistence and the “no lander wired” rejection behavior. |
| crates/ironclaw_product_workflow/src/webui_inbound.rs | Adds WebUiInboundAttachment, attachments field, and decode_attachments() decode/validation/budget logic. |
| crates/ironclaw_product_workflow/src/reborn_services/error.rs | Adds a public RebornServicesError::internal() constructor for external port impls. |
| crates/ironclaw_product_workflow/src/reborn_services.rs | Adds InboundAttachmentLander port + wiring in submit_turn to land bytes and persist attachment refs. |
| crates/ironclaw_product_workflow/src/lib.rs | Re-exports WebUiInboundAttachment and InboundAttachmentLander. |
| crates/ironclaw_product_workflow/Cargo.toml | Adds base64 and ironclaw_attachments dependencies for decoding + landing. |
| Cargo.lock | Updates lockfile for the added dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn normalize_attachment_mime(raw: &str) -> String { | ||
| raw.split(';') | ||
| .next() | ||
| .unwrap_or(raw) | ||
| .trim() | ||
| .to_ascii_lowercase() | ||
| } |
There was a problem hiding this comment.
Fixed — replaced the local normalize_attachment_mime with ironclaw_common::normalize_mime_type (identical split(';').next().trim().to_ascii_lowercase() logic, but now the single canonical normalizer the registry / kind inference / transcription already route through, so no drift). Good catch.
| // Land attachment bytes (if any) into project storage before the | ||
| // message is accepted, recording each as a transcript reference. | ||
| // Uses the stable per-message external_event_id for the storage | ||
| // path so a retry re-lands at the same deterministic location. | ||
| let message_content = if attachments.is_empty() { |
There was a problem hiding this comment.
Fixed the comment — it overclaimed determinism. The external_event_id is the path's message segment (stable across retries), but the lander also partitions by UTC day (Utc::now()), so a retry that crosses midnight UTC lands under the new day's directory, leaving the earlier bytes addressable-but-unreferenced. The comment now says exactly that, and notes idempotency is enforced at message acceptance (external_event_id dedup in the thread store), not by the storage path — so a cross-day retry is a dangling-bytes case, not a correctness bug.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_product_workflow/src/reborn_services.rs (1)
1647-1737:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy liftAdd a pre-storage safety scan in the new attachment ingress lane.
submit_turnnow accepts user attachment payloads and persists them vialander.land(...), but this path has no explicitsafety_layer.*scan before workspace/storage write.Suggested direction
pub struct RebornServices { thread_service: Arc<dyn SessionThreadService>, turn_coordinator: Arc<dyn TurnCoordinator>, inbound_attachments: Option<Arc<dyn InboundAttachmentLander>>, + inbound_attachment_scanner: Option<Arc<dyn InboundAttachmentScanner>>, ... } +#[async_trait] +pub trait InboundAttachmentScanner: Send + Sync { + async fn scan_pre_storage( + &self, + scope: &ThreadScope, + attachments: &[InboundAttachment], + ) -> Result<(), RebornServicesError>; +} ... let message_content = if attachments.is_empty() { MessageContent::text(content.clone()) } else { + let scanner = self + .inbound_attachment_scanner + .as_ref() + .ok_or_else(|| RebornServicesError::service_unavailable(false))?; + scanner.scan_pre_storage(&thread_scope, &attachments).await?; let lander = self .inbound_attachments .as_ref() .ok_or_else(|| RebornServicesError::service_unavailable(false))?; let refs = lander .land(&thread_scope, &external_event_id, attachments) .await?; MessageContent::with_attachments(content.clone(), refs) };As per coding guidelines, "Every new ingress point ... must run the matching safety scan on the pre-transform, pre-injection payload" and "Memory and workspace writes must apply injection scan on the pre-storage value, never on the transformed or rendered value."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_product_workflow/src/reborn_services.rs` around lines 1647 - 1737, The new attachment ingress path in the submit_turn method accepts user attachment payloads via request.decode_attachments() and persists them via lander.land() without running a safety scan. Before passing the attachments variable to lander.land(), apply the appropriate safety_layer injection scan on the pre-storage attachments payload to comply with the coding guidelines that require every new ingress point to run matching safety scans on pre-transform payloads and that memory/workspace writes must apply injection scans on pre-storage values. Add the safety scan call immediately before the lander.land() invocation where the message_content is being constructed with attachments.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_product_workflow/src/reborn_services.rs`:
- Around line 1286-1291: `InboundAttachmentLander::land` currently accepts
`message_id` as a raw `&str`, which should be replaced with a strong identifier
type at this public boundary. Update the `land` signature and all call sites to
use a dedicated newtype or existing identifier type such as `IdempotencyKey`,
and keep the typed identifier flowing through any related reborn service methods
instead of converting back to string.
In `@crates/ironclaw_product_workflow/src/webui_inbound.rs`:
- Around line 125-195: The decode_attachments method accepts and validates
user-controlled attachment payloads but does not perform a required safety_layer
scan on the pre-transform attachment data before returning them for downstream
storage. Add a call to the appropriate safety_layer scanning function on the
decoded attachment bytes or the original attachment payload before the method
returns the decoded attachments vector. Handle any safety scan errors by
converting them to WebUiInboundValidationError instances consistent with the
existing validation error handling pattern in the method, ensuring the error
includes an appropriate field identifier and validation code.
---
Outside diff comments:
In `@crates/ironclaw_product_workflow/src/reborn_services.rs`:
- Around line 1647-1737: The new attachment ingress path in the submit_turn
method accepts user attachment payloads via request.decode_attachments() and
persists them via lander.land() without running a safety scan. Before passing
the attachments variable to lander.land(), apply the appropriate safety_layer
injection scan on the pre-storage attachments payload to comply with the coding
guidelines that require every new ingress point to run matching safety scans on
pre-transform payloads and that memory/workspace writes must apply injection
scans on pre-storage values. Add the safety scan call immediately before the
lander.land() invocation where the message_content is being constructed with
attachments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 48d690c4-edd9-4f09-8ad8-9d199f3bd42a
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (17)
crates/ironclaw_product_workflow/Cargo.tomlcrates/ironclaw_product_workflow/src/lib.rscrates/ironclaw_product_workflow/src/reborn_services.rscrates/ironclaw_product_workflow/src/reborn_services/error.rscrates/ironclaw_product_workflow/src/webui_inbound.rscrates/ironclaw_product_workflow/tests/reborn_services_contract.rscrates/ironclaw_product_workflow/tests/webui_inbound_contract.rscrates/ironclaw_reborn_composition/CLAUDE.mdcrates/ironclaw_reborn_composition/Cargo.tomlcrates/ironclaw_reborn_composition/src/attachment_landing.rscrates/ironclaw_reborn_composition/src/lib.rscrates/ironclaw_reborn_composition/src/local_dev_mounts.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/src/webui.rscrates/ironclaw_reborn_composition/src/webui_body_limit.rscrates/ironclaw_webui_v2/src/descriptors.rscrates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs
…ment Address review: - Replace the local normalize_attachment_mime in webui_inbound with ironclaw_common::normalize_mime_type (identical logic, but the single canonical normalizer the registry/kind/transcription already share) so attachment MIME handling can't drift from the rest of the workspace. - Correct the attachment-landing comment in reborn_services: the external_event_id is the path's stable message segment, but the lander partitions by UTC day, so a retry crossing midnight UTC lands under a new directory (dangling earlier bytes). Idempotency is enforced at message acceptance, not by the storage path.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_product_workflow/src/reborn_services.rs (1)
1722-1739:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDo not reuse a pre-accept landing key that ignores attachment content.
land()runs beforeaccept_inbound_message()establishes the idempotent winner, but the landed path is reused for same-day retries byexternal_event_id. Two same-day submissions with the sameclient_action_idand different attachment bytes can overwrite the same files, so the accepted transcript refs end up pointing at whichever write won last. The same ordering also leaves orphaned files whenever landing succeeds andaccept_inbound_message()fails afterward. Please either land behind a durable accepted-message id, or make the landed key/content-addressing reject mismatched replays before any overwrite.As per coding guidelines, "A cache storing values dependent on input X must include a stable representation of X in the cache key; if get_or_create(a, b) uses only 'a' as key but 'b' affects the stored value, that is a bug."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_product_workflow/src/reborn_services.rs` around lines 1722 - 1739, The attachment landing path in reborn_services::land should not key storage only by external_event_id before accept_inbound_message() chooses the durable winner. Update the flow so landing is tied to the accepted message identity (or another content-addressed key) and rejects replay attempts whose attachment bytes differ from the already-landed content; this prevents same-day retries from overwriting files and avoids orphaned uploads when acceptance later fails. Use the existing land() and accept_inbound_message() sequence in reborn_services.rs as the place to introduce the stronger keying/check.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@crates/ironclaw_product_workflow/src/reborn_services.rs`:
- Around line 1722-1739: The attachment landing path in reborn_services::land
should not key storage only by external_event_id before accept_inbound_message()
chooses the durable winner. Update the flow so landing is tied to the accepted
message identity (or another content-addressed key) and rejects replay attempts
whose attachment bytes differ from the already-landed content; this prevents
same-day retries from overwriting files and avoids orphaned uploads when
acceptance later fails. Use the existing land() and accept_inbound_message()
sequence in reborn_services.rs as the place to introduce the stronger
keying/check.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 36260395-abce-4037-ab24-5a4d95e6bdd9
📒 Files selected for processing (2)
crates/ironclaw_product_workflow/src/reborn_services.rscrates/ironclaw_product_workflow/src/webui_inbound.rs
Re: CodeRabbit — "Do not reuse a pre-accept landing key that ignores attachment content" (outside-diff, reborn_services.rs:1722-1739)Verified against the full function; the described overwrite-of-accepted-content is not reachable, because the outside-diff view missed two guards above 1. Per-thread operation lock — 2. Replay short-circuit before So under the lock, submission A lands+accepts (recording the id) before B acquires it; B then hits the replay branch and returns A's accepted message without re-landing. An accepted message's bytes are written exactly once, in the same locked section that accepts them — there is no "last write wins" mismatch against an accepted ref. Genuine residual: orphaned files when Narrow theoretical gap: a cross-thread path collision requires a client to reuse one |
… path (nearai#4644) (nearai#4672) * feat(reborn): accept inline attachment uploads on the WebChat v2 send path (nearai#4644) End-to-end ingress wiring for nearai#4644: a browser can now attach files to a WebChat v2 message, the bytes land in project storage, and the user message persists attachment references. This is the call site for the land_inbound_attachments bridge. Flow (no src/ changes): - DTO: WebUiSendMessageRequest gains `attachments: Vec<WebUiInboundAttachment>` (mime_type, filename, base64). decode_attachments() validates MIME against the shared format registry (Track 1), decodes base64, and enforces the v1 budgets (5 MiB/file, 10 MiB total, 10 files max). Kept separate from into_command so the serializable command never carries raw bytes. - Facade: RebornServices::submit_turn decodes attachments, lands them through a new InboundAttachmentLander port (using the stable per-message external_event_id for the storage path), and builds MessageContent::with_attachments before accept_inbound_message. With no lander wired, an attachment-bearing message is rejected (503) rather than silently dropped. - Port impl: composition's ProjectScopedAttachmentLander writes through the project-scoped workspace ScopedFilesystem (the same authority the agent's file tools resolve through) via land_inbound_attachments, and is wired onto the facade in build_webui_services when a local runtime is present. - Body limit: the send_message route descriptor goes from 1 MiB to 14 MiB to carry base64 of the 10 MiB decoded cap; the descriptor/body-limit contract tests and the composition CLAUDE.md are updated to match. Tests: - decode_attachments: metadata/kind/bytes, MIME normalization, unsupported MIME, malformed base64, per-file/total oversize, too-many, empty. - ProjectScopedAttachmentLander: lands + returns ref with storage_key; read-only workspace mount maps to an internal error. - Facade (test through the caller): submit_turn lands the attachment and the accepted user message carries the ref with storage_key; attachments without a wired lander are rejected with ServiceUnavailable. New dep edges product_workflow/composition -> ironclaw_attachments (accepted by the reborn dependency-boundary test). Deferred: model-visibility (content_parts / extracted_text / project_path), the memory index note, and the static frontend "+ button" upload UI. * refactor(reborn): route attachment-landing errors through canonical constructors Reuse the error type's own constructors instead of open-coding status/kind: - submit_turn's no-lander path now calls RebornServicesError::service_unavailable(false) instead of an inline from_status_kind(Unavailable, ServiceUnavailable, 503, false). - Add a public RebornServicesError::internal() so host-composition adapters stop hand-rolling the full struct literal (which silently drifts if a field is added); ProjectScopedAttachmentLander uses it. - The lander no longer discards the underlying AttachmentLandingError: it logs it (warn) before mapping to the sanitized 500, so an operator can tell a misconfigured read-only mount from a write failure. - decode_attachments derives the per-attachment id from enumerate() rather than the decoded-accumulator length. * feat(reborn): cap inbound attachment size at the WebChat v2 lander land_inbound_attachments gained a required max_bytes bound (Track 6/nearai#4670). The ProjectScopedAttachmentLander now owns that policy — set to DEFAULT_MAX_ATTACHMENT_BYTES — and passes it per landing. The send_message route's 14 MiB body cap is the primary gate; this is defense in depth so a single attachment can never land unbounded bytes. * refactor(reborn): use canonical mime normalizer; fix landing-path comment Address review: - Replace the local normalize_attachment_mime in webui_inbound with ironclaw_common::normalize_mime_type (identical logic, but the single canonical normalizer the registry/kind/transcription already share) so attachment MIME handling can't drift from the rest of the workspace. - Correct the attachment-landing comment in reborn_services: the external_event_id is the path's stable message segment, but the lander partitions by UTC day, so a retry crossing midnight UTC lands under a new directory (dangling earlier bytes). Idempotency is enforced at message acceptance, not by the storage path.
Summary
End-to-end ingress wiring for #4644 — the call site for the
land_inbound_attachmentsbridge (#4670). A browser can now attach files to a WebChat v2 message; the bytes land in project storage through the filesystem authority, and the user message persists attachment references.Scope:
crates/only, nosrc/changes. Crossesironclaw_product_workflow(DTO + facade + port),ironclaw_reborn_composition(port impl + wiring), andironclaw_webui_v2(body limit).Flow
webui_inbound.rs):WebUiSendMessageRequestgainsattachments: Vec<WebUiInboundAttachment>(mime_type,filename,data_base64).decode_attachments()validates MIME against the shared format registry (Track 1), decodes base64, and enforces the v1 budgets (5 MiB/file, 10 MiB total, 10 files max). Kept separate frominto_commandso the serializable command never carries raw bytes.reborn_services.rs::submit_turn): decodes attachments, lands them through a newInboundAttachmentLanderport (using the stable per-messageexternal_event_idfor the storage path), and buildsMessageContent::with_attachmentsbeforeaccept_inbound_message. With no lander wired, an attachment-bearing message is rejected (503) rather than silently dropped.attachment_landing.rs):ProjectScopedAttachmentLanderwrites through the project-scoped workspaceScopedFilesystem— the same authority the agent's file tools resolve through — vialand_inbound_attachments, and is wired onto the facade inbuild_webui_serviceswhen a local runtime is present (mirrors theApprovalInteractionServiceinjection idiom).send_messageroute descriptor goes 1 MiB → 14 MiB to carry base64 of the 10 MiB decoded cap. Descriptor/body-limit contract tests and the composition CLAUDE.md are updated to match.Tests
decode_attachments(7): metadata/kind/bytes + MIME normalization; unsupported MIME; malformed base64; per-file oversize; total oversize; too-many; empty.ProjectScopedAttachmentLander(2): lands + returns ref withstorage_key; read-only workspace mount maps to an internal error.submit_turnlands the attachment and the accepted user message carries the ref withstorage_key; attachments without a wired lander are rejected withServiceUnavailable.New dep edges
product_workflow/composition→ironclaw_attachments, accepted by the reborn dependency-boundary test.Stacking
Based on
fix/4644-inbound-attachment-refs(#4670). Tip of the #4644 epic stack (registry → transcript refs → byte landing → bridge → this ingress wiring).Deferred (remaining #4644 work)
storage_key→ChatMessage.content_partsfor images; run extraction/transcription to fillextracted_text; emit model-facingproject_path; fixchat_workflow.rs's[non_text_content]drop. (Doc/audio extraction still needs thesrc/→crate move you flagged.)memory_searchdiscoverability); sandbox e2e; WASMstore_attachment_dataconvergence.Summary by CodeRabbit
New Features
Bug Fixes
Documentation