Skip to content

feat(plugins): api.attach gives an agent a file, and api.localPaths - #181

Open
Stener1 wants to merge 3 commits into
cosscom:mainfrom
Stener1:plugin-api-attach
Open

Stener1 wants to merge 3 commits into
cosscom:mainfrom
Stener1:plugin-api-attach

Conversation

@Stener1

@Stener1 Stener1 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Plugins can give an agent a file the way the composer does. berth.api gets two methods. Both go through the composer's own helpers in lib/attachments.ts, so there is no second upload path:

api.attach(box, { session } | { location, worktree }, file, { onProgress, signal, pasted })
  file: File
    pasted ? shrinkImage(file)              # a big screenshot goes as WebP, as the composer's does
    uploadAttachment(client, target, file)  # same names, 20 MB refusal, .json as bytes
  file: string (a path on this computer)
    box at 127.0.0.1?  → the path as it is (the box's own file), unread, size 0
    else               → uploadLocalFile(client, target, path, signal)   # the laptop agent reads it
api.localPaths(text)    → the composer's localPaths: [] unless the paste is all laptop paths
  • Attachment is now declared in @berth/plugin; the app re-exports it.
  • Neither method is a named runtime export. A plugin feature-detects with typeof berth.api.attach === "function", so it still loads on an older app.
  • Client.laptop takes an optional signal, so an aborted attach of a path uploads nothing.
  • Docs: docs/reference/plugin-sdk.mdx (api table), the SDK README, the changelog.
  • verify-berthd: a plugin-attach-live scenario, features/plugins.md plugin-attach, and dev-box.sh takes VERIFY_BOX_ADDR so the dev box can count as another computer.

Evidence

Unit: app/src/lib/attachments.test.ts covers the session route, the worktree route, the pasted name, .json sent as octet-stream, the 20 MB refusal with no upload, a path going through attach-local for both targets, a path on a local box resolving unread, the signal reaching the laptop call, pasted on a text file, and localPaths. pnpm test: 586 pass, 0 fail. typecheck:plugins, every check:*, build, tsc -p e2e and the docs-site build all pass. No Go changed.

End to end: verify-berthd plugin-attach-live. A test plugin is loaded as a built-in, on a dev box and agent of this checkout, through the real app build. The console was empty in both runs.

Box paired at 127.0.0.1 Box paired at the tailnet IP (another computer)

Steps from result.json (remote run):

  • A picked notes.md attached to session ask lands at acme-shop/.berth/attachments/20261022-…-notes.md, with progress 50/50.
  • A picked image.png attached to a new worktree lands as …-pasted-HHMMSS.png.
  • A 21 MB log is refused with "huge.log is 21 MB; attachments can be up to 20 MB.", and no file arrives.
  • localPaths("see /tmp/shot.png") gives []. localPaths("file:///tmp/verify-attach-…/shot.png") finds the path, and attaching it uploads through the laptop agent, byte for byte. On the loopback box the same call resolves to the path itself, and nothing arrives.
  • The same path with an already-aborted signal is refused with AbortError, and nothing arrives.
  • A 7.4 MB 2400×1600 PNG sent with pasted: true goes up as a 1.9 MB WebP.
  • Second view, the box's disk: every file matches what was picked byte for byte, and git check-ignore ignores each one.

Merge Danger

Door: two-way

Additive SDK surface. The only change to an existing signature is an optional trailing signal on Client.laptop.

Blast Radius: plugins

A plugin that calls attach on an older app finds no function, which is the documented feature check. The composer's own attach paths are untouched apart from uploadLocalFile taking an optional signal they don't pass.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

Stener1 and others added 3 commits October 23, 2026 22:49
… does

A plugin can upload a pasted or dropped file into a running session's
worktree, or a worktree an agent is about to start in, and put the path it
resolves with in a prompt. It reuses the composer's upload: the same names,
the same 20 MB refusal, .json as plain bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reenshots; api.localPaths

A plugin can attach a file a paste names on this computer, found in the
pasted text by api.localPaths: the laptop agent uploads it, and a box on
this computer gets the path as it is. pasted: true shrinks a screenshot
first, as the composer does. The verify skill's dev box can listen on
another address (VERIFY_BOX_ADDR), so a path's upload is driven for real.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attach with a path on this computer now passes opts.signal to the laptop
agent's request, so an aborted attach uploads nothing (Client.laptop takes
a signal).

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

This branch has not been deployed

No deployments
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