Skip to content

Add temporary chat file attachments - #245

Merged
mattprintz merged 3 commits into
devfrom
file-upload-chat
Jul 20, 2026
Merged

Add temporary chat file attachments#245
mattprintz merged 3 commits into
devfrom
file-upload-chat

Conversation

@brandomr

Copy link
Copy Markdown
Contributor

Summary

Adds temporary, session-scoped file attachments to the notebook chat interface.

Users can drag files onto the chat composer or select them with the attachment button. Files are uploaded before the message is sent, displayed as removable attachment chips, and made available to the agent for the duration of the notebook session.

ZIP archives are extracted automatically using configurable safety limits. The original ZIP is retained so the agent can inspect or troubleshoot it if automatic extraction fails.

Attachments remain separate from the notebook filesystem and are not serialized into the notebook document.

User experience

  • Drag and drop one or more files onto the chat composer.
  • Select multiple files through the attachment button.
  • See upload progress, filenames, sizes, ZIP extraction status, and errors.
  • Remove an attachment before sending by clicking its X button.
  • Cancel an upload that is still in progress.
  • Send attachments with or without accompanying text.
  • When no text is supplied, the query uses: Please inspect the attached file(s).
  • See sent attachments displayed on the corresponding query cell.
  • Upload multiple individual files or ZIP archives in the same message.

The existing file browser remains the mechanism for permanent notebook files. Chat attachments are intentionally temporary.

Storage and lifecycle

Attachments are stored beneath a hidden runtime directory:

.beaker/session-attachments/<server-instance>/<session-hash>/<attachment-id>/

Each attachment has:

  • A generated attachment ID.
  • Session ownership metadata.
  • The original filename and media type.
  • A committed/draft state.
  • A file manifest.
  • Extracted ZIP metadata when applicable.
  • Exact server-side paths for agent access.

Files are scoped to the owning user and notebook session.

Draft attachments become committed when their IDs are included in an LLM request and retrieved by the authenticated kernel. Attachments from prior messages remain available to the agent for the rest of the session.

Cleanup occurs when:

  • A draft attachment is removed from the composer.
  • The notebook session is explicitly deleted.
  • The Beaker server shuts down normally.

The runtime attachment directory is ignored by Git so uploaded files cannot be committed accidentally.

ZIP handling

ZIP files are detected by filename or MIME type and extracted into the attachment's temporary directory.

Extraction rejects:

  • Absolute paths and .. traversal.
  • Duplicate archive paths.
  • Symbolic links.
  • Encrypted files.
  • Special/non-regular files.
  • Archives exceeding the configured file-count limit.
  • Archives exceeding the configured expanded-size limit.
  • Archives exceeding the configured compression-ratio limit.

Extraction is performed in a temporary directory and moved into place only after it succeeds.

If extraction fails:

  • The extracted directory is cleared.
  • The original ZIP is retained.
  • The failure reason is exposed to the UI and agent.
  • The agent can inspect or debug the original archive as needed.

Agent integration

The query message sends attachment IDs to the Beaker kernel.

Before starting the agent loop, the kernel:

  1. Authenticates to the server.
  2. Resolves the current notebook session.
  3. Verifies that the kernel owns the session.
  4. Commits the attachment IDs included in the request.
  5. Loads the session's committed attachment metadata into the context.

The agent receives a conditional session_attachments state block containing:

  • Attachment IDs.
  • Filenames and media types.
  • Current-message status.
  • Exact paths.
  • ZIP extraction status.
  • A bounded archive manifest.

The agent also receives a get_session_attachment tool that:

  • Returns bounded text previews for common text formats.
  • Returns multimodal content for supported image, audio, and video files.
  • Provides exact paths for structured or binary processing through run_code.
  • Prevents relative paths from escaping an extracted attachment directory.

This allows workflows such as uploading a CSV and asking the agent to analyze it without first copying it into the permanent notebook filesystem.

Authentication and ownership

Attachment API operations resolve the notebook session and enforce either:

  • Normal authenticated-user ownership, or
  • A verified Beaker kernel authentication header matching the session's kernel.

The handler does not trust a kernel ID parsed directly from a request header. The identity provider verifies the token first and returns the authenticated kernel ID. Digest comparison uses hmac.compare_digest.

Invalid or forged kernel headers return HTTP 403 rather than falling back to browser authentication.

A broader redesign of the existing kernel authentication protocol—such as HMAC tokens, nonce expiration, and replay protection—is intentionally left for a separate authentication-focused PR.

Configuration

The following settings are configurable through Beaker configuration or environment variables:

Setting Environment variable Default
Maximum upload size per file ATTACHMENT_MAX_UPLOAD_BYTES 25 MiB
Maximum stored size per session ATTACHMENT_MAX_SESSION_BYTES 250 MiB
Maximum extracted ZIP size ATTACHMENT_MAX_EXTRACTED_BYTES 100 MiB
Maximum ZIP file count ATTACHMENT_MAX_ARCHIVE_ENTRIES 1,000
Maximum ZIP compression ratio ATTACHMENT_MAX_ARCHIVE_RATIO 100:1

Testing

Automated verification completed:

  • Full Python test suite: 390 passed
  • Beaker TypeScript type-check
  • Beaker Vue type-check
  • ESLint: no errors
  • Playwright attachment end-to-end test
  • git diff --check

The Playwright test covers:

  • Drag-and-drop upload.
  • Multiple attachments.
  • Removing an uploaded attachment.
  • ZIP extraction and file-count display.
  • Sending an attachment without text.
  • Rendering attachments on the query cell.
  • Confirming that the sent attachment becomes committed.

Manual testing also covered individual files, multiple files, ZIP archives, and agent access to uploaded content.

Follow-up hardening

These are intentionally visible for reviewer discussion:

  • Enforce the per-file limit while streaming the HTTP request. Tornado currently parses the multipart body before the attachment manager applies its configured limit.
  • Consider returning redacted attachment metadata to browser clients so absolute server paths are only visible to the authenticated kernel.
  • Add age-based startup cleanup for attachment directories left behind after a crash or forced process termination.
  • Consider per-session locking rather than a manager-wide mutation lock for highly concurrent deployments.
  • Read only the configured preview size for text attachments instead of reading the complete file before truncating it.

Out of scope

  • Permanent uploads to the notebook file browser.
  • Persisting attachments in notebook documents or snapshots.
  • Installing uploaded Agent Skills.
  • Dynamically registering skill ZIPs with the agent's skill provider.
  • Archive formats other than ZIP.
  • Redesigning the existing Beaker kernel authentication protocol.

@brandomr
brandomr requested a review from mattprintz July 14, 2026 19:51
brandomr and others added 2 commits July 14, 2026 15:10
Two things that I think would be helpful:

1) SessionAttachmentManager._session_root roots everything at _user_root(user), which for a non-BeakerUser request falls back to root_dir — i.e. the working directory. In local mode (BeakerNotebookApp), that means running beaker notebook in any shell drops a .beaker/session-attachments/ tree into whatever CWD you launched from (which is also why the PR needs the new .gitignore entry). For a "temporary" store, I'd rather not scatter state across arbitrary working directories.
  - Suggestion: key the location on the app rather than on request identity, and send local-mode attachments to the shared data dir we already use for notebook snapshots (BEAKER_LOCAL_DATA_PATH). I've got a small patch that adds a local_mode property (False on BaseBeakerApp, True on BeakerNotebookApp) and branches placement in a _attachments_base() helper — local →  BEAKER_LOCAL_DATA_PATH/session-attachments, server → the existing per-user virtual-home path.
2) Also tightens cleanup() so it doesn't remove dirs above the session-attachments dir. The two-level climb could remove the shared data root, potentially causing a race condition where one session tries to write to a path that it thinks exists but has just been deleted.
@mattprintz

Copy link
Copy Markdown
Collaborator

This is very cool and very slick and works well.
I provided some feedback via Slack regarding a couple relatively small suggestions.

@mattprintz
mattprintz merged commit c89dc77 into dev Jul 20, 2026
4 checks passed
@mattprintz
mattprintz deleted the file-upload-chat branch July 20, 2026 17:37
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