Skip to content

Carry upstream's T3 attachment toolkit over the file bridge - #294

Merged
henrychu04 merged 2 commits into
coder-onlyfrom
t3/attachment-toolkit-bridge
Oct 31, 2026
Merged

henrychu04 merged 2 commits into
coder-onlyfrom
t3/attachment-toolkit-bridge

Conversation

@henrychu04

Copy link
Copy Markdown
Owner

Restores upstream's T3 attachment toolkit (t3_thread_send_attachments, t3_attachment_prepare_upload, t3_attachment_discard) on the workspace file bridge, so an agent can hand images and files to another thread, for example a screenshot for a delegated task. This also re-enables pending uploads in t3_thread_launch / create_threads initial messages, which already accepted them through upstream's project toolkit.

Carried 1:1 from upstream (80a2351)

  • apps/server/src/mcp/toolkits/attachment/handlers.ts and tools.ts: the discard and send tools, resolveAttachmentReferences (resending an attachment the target thread already owns needs no upload), upload-owner tracking and TTL, and every name and schema except the upload step below.
  • Registered in T3ToolBridge.ts the same way upstream's McpHttpServer.layerAttachmentToolkit registers them; the bridge's --list and --schema come from the tool definitions, so the bridge instructions text needed no change.
  • Tests: upstream's core.test.ts attachment cases (resolves reused attachment references…, only the caller that prepared a pending upload can discard it) now run in attachment/handlers.test.ts against the toolkit directly, because the fork has no McpServer.

Fork deltas (marked Coder:, listed in coder-only.md seams)

  • t3_attachment_prepare_upload takes a path beside upload. Upstream returns a URL for the agent to POST to on "this MCP server's HTTP origin", and the helper has no listener (boundary 6). The handler now:

    • mints the token with upstream's issueAttachmentUploadUrl;
    • checks that path is an absolute path to a regular file of exactly upload.sizeBytes;
    • validates the token with validateAttachmentUploadToken and streams the file through storeAttachmentUpload, which is the same validation and store path as upstream's upload route.

    The agent and helper share the workspace filesystem, so the bytes never go through the bridge's 256 KiB request or response files. Upstream's byte limits still apply: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / PROVIDER_SEND_TURN_MAX_FILE_BYTES in the upload schema, the exact-size store, and image signature checks when the attachment is claimed.

  • The result is { attachmentId }, with no URL, because the upload is already stored. The tool description changes to match.

  • An upload forwarded through the gateway hub (client.linked) is refused: its path would name a file in the other workspace.

Cross-workspace behaviour

Upstream's attachment tools take no environmentId, so McpToolAccess never routes them to a linked environment. A threadId in a linked workspace therefore answers upstream's thread_not_found. A prepare call forwarded through the hub gets invalid_request with a message to upload from a thread in that workspace. No new transfer path is added.

Upstream sync

coder-only had not moved when this was pushed (base 52c7e7a). git diff 80a2351fcb 15fe283272 is empty for the carried files and the code they call (toolkits/attachment/, AttachmentUpload.ts, McpToolAccess.ts, ThreadMessageIntake.ts, AttachmentClaims.ts, contracts/assets.ts), so merging the 15fe283 sync needs no bring-up here.

Verification

  • Scratch end-to-end test, run and then deleted, not committed. It used a real orchestrator, the real bridge CLI over sh, and a fake provider:
    • --list and --schema show the tools.
    • prepare_upload from a workspace PNG path, then t3_thread_send_attachments to a second thread: the target message has one claimed thread-… image whose bytes on disk match the file.
    • Resending the target's own attachment id works with no upload.
    • Discard after send succeeds.
    • Refusals checked: relative path, missing file, directory, wrong size, a thread id not in this workspace (thread_not_found), and a hub-forwarded prepare.
  • All AGENTS.md typechecks pass.
  • pnpm test:server: 3559 passed, 2 failed, both known ClaudeSkills plugin-order test fails on macOS (/var symlink) #235 (ClaudeSkills, GitVcsDriverCore on macOS).
  • pnpm test:providers, pnpm test:contracts, pnpm test:knip, node scripts/seams-check.mjs, vp fmt --check on changed files, and pnpm build all pass.
  • The live Coder harness was not run; the parent runs it.

🤖 Generated with Claude Code

henrychu04 and others added 2 commits October 31, 2026 13:06
t3_thread_send_attachments, t3_attachment_prepare_upload, and
t3_attachment_discard come from upstream verbatim except the upload step:
with no MCP HTTP origin to POST to, prepare_upload takes the absolute
workspace path of the file and stores it through upstream's upload
validation and store in process, so bytes never pass through the bridge's
bounded request files. Uploads forwarded from a linked workspace are
refused; sends reach only threads in this workspace, as upstream's tools
take no environmentId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@henrychu04

Copy link
Copy Markdown
Owner Author

Merged coder-only (#293) into this branch. Reviewed the path-staging delta: absolute path, regular file, size must equal upload.sizeBytes, bounded read through upstream's storeAttachmentUpload; uploads forwarded from a linked workspace are refused. Live check on the local Coder harness with a fake kilo acp agent driving the real bridge CLI: t3_attachment_prepare_upload staged a 32-byte workspace file and returned an attachment id, a relative path was refused with invalid_request, and t3_attachment_discard succeeded. Server/helper typechecks, test:server (only the #235 macOS pair plus a ClaudeAdapterV2 todo_list timeout that also fails on 7b64141 under current machine load), knip, fmt, seams-check and build pass.

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.

1 participant