Feature: File attachments - #460
Open
CFDan wants to merge 2 commits into
Open
Conversation
Adds twprojects-create_file, which takes a file as base64 and returns a single-use reference, and an attachment_refs parameter on create_task, update_task, create_comment and create_message that consumes it. Two calls rather than one keeps the file out of the message that creates the task, so a failed create does not mean re-sending the payload. Uploading is capped at 5 MB decoded, checked before decoding so an oversized payload is rejected without allocating it twice. The real ceiling is far lower: the caller has to emit the base64 itself, at roughly one output token per three bytes. Adds internal/logsafe and routes the request body and tool argument log sites through it. File content is replaced before the payload is capped, because base64 usually leads the arguments object and truncation alone would keep the readable start of a customer's document while cutting the tool name and the other parameters. Outbound bodies that are not text are elided rather than read, which also stops twdesk-create_file writing raw file bytes into the log as it does today. Attachments are additive. attachmentOptions is not sent: its only field defaults to false, and on task update the API seeds the "keep these" list from the task's current attachments, so it removes nothing. Tool definitions grow by 553 tokens, 1.10%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Redaction now catches a file of any size: the value class is "anything but a quote" with no length floor, so a small secret no longer survives because its base64 is short, and it is not defeated by JSON escaping such as "\/". "content" is dropped from the redacted keys, since page and comment bodies use it for text worth keeping. A byte-scan fast path skips the regex for the payloads that carry no file, which is almost all of them. The logging round tripper no longer scans outbound API traffic for file content. The only file it can see is the multipart upload, which the content-type gate already elides, so the scan had no reachable file to catch and could only corrupt an unrelated response body that happened to carry a long value under one of those keys. create_file moves to the projects toolset. attachment_refs is exposed by tools in both the tasks and content toolsets, and a project-level file is the right home for the tool that mints the references they consume. create_comment and create_message route attachment_refs through the same cleaner the task tools use, so a blank reference is dropped consistently rather than forwarded to the API. The filename assertion in the tests drops the bespoke multipart parser for the substring check the sibling test already uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Ran a full review (correctness + reuse/simplification/efficiency/altitude) and pushed 5e15dfd. Fixed
Checked and left as-is
Reminder: still gated on twapi-go-sdk#117 — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
Depends on Teamwork/twapi-go-sdk#117. Do not merge before it.
go.modcurrently points at a commit on that PR's branch (v1.21.4-0.20260815115947-d48d296bd66e) because the SDK functions this uses are unreleased. Once #117 merges and is tagged, this needsgo get github.com/teamwork/twapi-go-sdk@<new tag>and a fresh commit before merging.Lets an LLM attach a file to a task, comment or message. This is the customer request behind #117: models generate plans and specs and currently have no way to put them on a task.
Shape
attachment_refsis also onupdate_task,create_commentandcreate_message. Two calls rather than one keeps the file out of the message that creates the task, so a failed create does not mean re-sending the payload.Notes for review
The log redaction in
internal/logsafeis a prerequisite, not a nice-to-have.LoggingRoundTripperreads every outbound body in full, sotwdesk-create_fileis writing raw file bytes into the logs today — this fixes that as a side effect. Content is replaced before the payload is capped, because base64 usually leads the arguments object: truncating alone would keep the readable start of a customer's document and cut the useful part of the record. Non-textual outbound bodies are elided rather than read at all.No
urlparameter, deliberately. On a hosted multi-tenant server that is an SSRF with a direct exfiltration path — a prompt-injected model asks for169.254.169.254, the server fetches from inside the VPC, and the response is stored as an attachment the attacker can read. Doing it safely needs resolved-IP checks, a re-check at connect time to beat DNS rebinding, a per-hop redirect policy andOpenWorldHint: true. Base64 only.5 MB decoded cap, and
maxBodySizeis unchanged. Raising it would not help: the binding constraint is the caller's output-token budget, not the transport, at roughly one output token per three bytes of file. The size is checked on the encoded length before decoding, so an oversized payload is rejected without being allocated twice — pinned by a test asserting no HTTP request is made.attachmentOptionsis not sent. Its only field defaults tofalse, and on task update the API seeds the "keep these" list from the task's current attachments, so it removes nothing. Attaching is additive. There is also notwprojects-list_files, so a caller could not obtain an identifier to detach with.No
mime_typeparameter, unliketwdesk-create_file. The upload endpoint derives the type from the extension server side, and the Desk model genuinely stores one where this does not.Testing
internal/twprojects/files_test.goasserts the parameters reach the wire: that the upload is multipart with the decoded bytes under a part namedfile, thatattachmentsis a sibling oftaskrather than one of its attributes, that comment and message sendpendingFileAttachments, and that noattachmentskey is sent when the caller asked for none. Filename sanitisation is table-driven over path, traversal and control-character inputs. Bad input comes back as a readable tool result rather than a transport error.internal/logsafehas its own tests, including one that runs an 8 MB body through the pattern to guard a future rewrite from introducing backtracking.Tool definitions grow by 553 tokens, +1.10%, measured with
cmd/mcp-tokens.The underlying API calls were verified live against a real installation as part of #117. I could not drive the MCP server itself end to end here: it authenticates with a bearer token and the API token I had is not valid as one, so the coverage above is the mock-level wire assertions rather than a live round trip.
🤖 Generated with Claude Code