First release. dsh plugin --profile web add dsh-agentmail
Gives a DeepSeek Harness agent its own email inbox through AgentMail. Four independent Cordis plugins — tools, identity, approval, inbound.
What's in it
Eleven tools for sending, reading, searching, drafting and labelling mail. Sends are idempotency-keyed on the tool call id, so a retried call can't double-deliver.
Inbound mail bound to one session per email thread. The session id is a pure function of the thread id, so there is no local mapping store — a thread session that doesn't exist yet rebuilds itself from threads.get. Disposal is therefore non-destructive.
Follow-ups that outlive the conversation. agentmail_followup writes a due-date label onto the thread; a periodic sweep revives exactly those sessions. Harness Schedule reminders only fire while a session has a live root Agent, which is precisely what's absent when nobody replied.
Outbound is gated. Approval on by default, optional recipient allowlist enforced through the monotonic ctx.tools.guard(), and denial happens before the approval prompt so the outcome doesn't depend on pipeline ordering.
Inbound bodies are untrusted. Fenced, with the fence sequence neutralized inside the body, and the system-prompt section tells the model that text inside the fences is data rather than instructions.
Verification
75 unit tests, plus a harness-test/ suite that boots the plugin in a real Cordis composition with the actual harness service packages. Verified end to end against the live AgentMail API: real mail → WebSocket event → per-thread session created with seeded history.
Running it for real found four bugs that fakes could not, each documented in the README's implementation notes — including websockets.connect() resolving already-OPEN (so an on('open') handler registered after the await never fires, and no inbound mail ever arrives) and Cordis enforcing inject by throwing rather than returning undefined.
Known gaps
The tools have never been driven by an actual model — verified through the real tool pipeline, but no LLM has chosen to call one. The bounce path needs a genuine hard bounce to exercise. threads.search relevance ordering is unverified.