feat: meeting attachments (issue #16) - #17
Conversation
.agents/.claude/_bmad/.history stay local. C6b probes the host for the millet CLI, breaking the all-clean doctor assertion off darwin/PATH. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST/GET/GET-download /api/sessions/<id>/attachments. Storage is the filesystem: sessions_dir/<id>/attachments/ is exactly what millet sync gets, and millet 0.15.0 pushes that subdir verbatim -- no DB column, no migration, no extra sync work. Not on /artifact/<id>/<name>: it rejects "/" and user filenames collide with millet's canonical artifact names in that flat namespace. Filenames flattened to one safe component (traversal, windows paths, control chars, length), collisions get _N suffixes, symlinks never listed or served. Caps mirror millet's (50 files / 100 MB per session, per-file max_upload_bytes) so nothing accepted here is dropped at sync; a failed request removes everything it wrote. sessions._enforce_team_visibility -> enforce_team_visibility (now used across modules). millet-pipeline floor >=0.15.0. 19 new tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… parts C-E) — 0.13.0 scribe: fixed staging folder ~/vezir-attachments (VEZIR_ATTACHMENTS_DIR) printed at record start; after stop it lists staged files and waits for Enter (skipped without a TTY or with --no-pause; Ctrl-C/EOF continues rather than discarding a recorded meeting), POSTs them once the session id is known, then moves them into the recording's own attachments/. Failures warn and leave the files staged; the meeting upload never fails because of them. api: list/download/save_attachment. TUI: marked attachment rows in the detail table, opened via ArtifactScreen(is_attachment=True). pull: attachments fetched into <meeting>/attachments/, names verbatim, wire names with a path rejected. README + CHANGELOG; version 0.13.0. 28 new tests, suite 925 -> 953. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Verdict: approve-with-comments — validation is strong end-to-end; one scope question (TUI record screen) and one minor edge case below. Tested by temporarily deploying this branch to our server (saray, now Verified workingAutomated (against real millet 0.15.1, not a stub):
Live end-to-end (saray 0.13.0 + client 0.13.0):
Sync leg over real infra: being confirmed by a Blink-team member (Blink has a git sync target; our Twentyone team has none, so that run stayed local-only — Question — TUI record screen has no attachment supportThe staging-folder + pause + upload flow lives only in the CLI Minor — same-box duplicate edge case
Nice, careful work — the security surface (traversal/symlink/caps/visibility) and the millet-boundary reasoning are thorough. |
Review follow-ups on PR pretyflaco#17. TUI record screen was the gap: it could view attachments but not attach while recording. It now shows the staging folder + staged count, prompts with the same last-chance list when recording stops (Enter/Escape continue, r rescans), and uploads them with the meeting. Workflow extracted to vezir/client/attachments.py so scribe and the TUI share one implementation; reporting goes through on_info/on_error so the CLI prints and the TUI posts messages. Move-into-recording now skips a byte-identical destination copy: same-box deployments with VEZIR_RECORD_DIR inside VEZIR_DATA/sessions otherwise got _2 duplicates of what the server just stored. 4 new tests, suite 953 -> 957. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the deployment run — especially for driving the real TUI record screen — wired, not deferredYou're right that it's a real gap, so it's in scope now rather than a follow-up.
To avoid two copies of the workflow drifting, it moved into a shared Same-box duplicates — guarded, not just noted
Tests4 new (suite 953 → 957, ruff clean): the prompt's staged-file listing and its always-continue dismissal, the record screen's folder/count line, and the identical-destination dedupe. Still outstanding from your review: the real-infra sync leg on a team that has a git target. The mechanism is covered by the automated |
|
Tested and works |
Implements the attachment-folder workflow from #16. Plan and rationale: #16 (comment)
All parts (A–E) are in. Still draft pending an end-to-end run on a real recording.
Part A — millet (prerequisite, merged + released)
pretyflaco/millet#28,
millet-pipeline0.15.0/0.15.1.millet syncpushes a session'sattachments/subdirectory into the team repo verbatim, bypassing its suffix allowlist and descriptive-rename map — which would otherwise have turned an attachedslides.pdfintotranscript.pdfand dropped every image, office document and video.Part B — server (
dd70229)vezir/server/attachments.py:POST/GET/GET <name>under/api/sessions/{id}/attachments.sessions_dir/<id>/attachments/, exactly the directory the worker hands tomillet sync. A test asserts that path agrees withmillet.sync.ATTACHMENTS_SUBDIRand that_collect_files()picks it up, so the two repos can't drift apart silently./artifact/{id}/{name}: that route rejects/, and user filenames would collide with millet's canonical artifact names in its flat namespace.C:\…paths, control characters, length cap that keeps the extension);_Nsuffixes on collision; symlinks never listed or served; team + personal-session visibility checked against the job row before any filesystem access; a failed request removes everything it wrote.max_upload_bytes()) viaconfig.max_attachments_per_session()andconfig.max_attachment_bytes_total()— accepting more would store files that sync silently drops.sessions._enforce_team_visibilitypromoted toenforce_team_visibility;millet-pipelinefloor raised to>=0.15.0.Parts C–E — client (
dce327c)scribe. Recording starts by printing a fixed staging folder,
~/vezir-attachments/(VEZIR_ATTACHMENTS_DIR) — one well-known path rather than one per recording, since dropping files starts before the recording's own directory name is settled. When recording stops, scribe lists what it found and waits for Enter as a last chance to add more; the pause is skipped when stdin is not a TTY (scribe is documented for headless/ssh use) or with the new--no-pauseflag. Ctrl-C or EOF at that prompt continues into the upload rather than discarding an already-recorded meeting. Once the session id is known the files are POSTed, then moved into that recording's ownattachments/so the staging folder is empty next time. A failed POST warns and leaves the files staged; a failed move warns. Neither fails the meeting upload.TUI. Attachments appear as marked rows in the detail screen's artifacts table and open through the existing
ArtifactScreen(inline text, OS opener for binaries, save-to-disk), which gained anis_attachmentflag selecting the attachment endpoint. A server without the route degrades to "no attachments" instead of blanking the view.pull.
download_session_artifacts()also fetches attachments into<meeting>/attachments/, names kept verbatim — pull is the "share a meeting without git" path and would otherwise omit what the git archive carries. A name arriving from the wire with a path in it is rejected before anything is written to the user's disk.Plus
VezirClient.list_attachments/download_attachment/save_attachment, README section and env-var rows, CHANGELOG entry, version 0.13.0.Accepted limitations (also in the CHANGELOG)
Tests
47 new tests across
tests/test_attachments.py(server round trip, storage path, cross-team and personal 404s, hostile filenames, symlinks, collisions, all three caps and their cleanup),tests/test_scribe_attachments.py(staging folder, pause behavior incl. non-TTY and Ctrl-C, upload-then-move, failure handling, request shape) andtests/test_attachments_client.py(API methods, pull, TUI row marking and dispatch).Full suite: 953 passed (906 before);
ruff check .clean.Not done here
🤖 Generated with Claude Code