Skip to content

Give hey mcp its own sign-in, and send a draft only on the person's confirmation - #598

Merged
jeremy merged 22 commits into
mainfrom
mcp-own-credential
Nov 5, 2026
Merged

jeremy merged 22 commits into
mainfrom
mcp-own-credential

Conversation

@jeremy

@jeremy jeremy commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Ship order:

  1. haystack #9215 merged and deployed: it pins HEY MCP (local) to client id vd96nBiRFXz1AjTXbADAtkrg in every environment (Oauth::Client::Mcp::LOCAL_MCP_CLIENT_UID).
  2. The production row is confirmed converged on that id.
  3. This merges.

hey mcp gets its own least-privilege sign-in, and it can send, with a confirmation for each email. This is the hey-cli lane of haystack #9041 (the local_mcp row) and #9085 (the scoped device flow). It implements sending-safety.md D1–D3 for stdio, as the hosted server does. D4 and D6 were withdrawn on 2026-10-31 and aren't here.

Release notes

hey mcp now has its own sign-in. Run hey auth login --mcp once; until you do, hey mcp exits with Run: hey auth login --mcp, even when the CLI is signed in or HEY_TOKEN is set. It can now send a draft, only after you confirm that email in your MCP client.

What changes for a person

hey auth login --mcp                  # once: "HEY MCP (local)", read write send
claude mcp add hey -- hey mcp
  • Without that sign-in, hey mcp exits with Run: hey auth login --mcp. That includes a person signed in to the CLI or with HEY_TOKEN set. Existing hey mcp registrations stop working until the person runs the sign-in once (release notes above). The hey CLI's commands and its own credential are unchanged.
  • hey_send / send_draft delivers a draft after the person confirms that exact email in their MCP client. It's listed only to a client that can show a confirmation. Any other client is pointed at the draft's page in HEY.

1. hey auth login --mcp

  • Separate storage. The credential lives in its own keychain item, hey / hey::mcp::<origin>, beside the CLI's hey::<origin>. With no keychain it gets its own entry in credentials.json. Its refreshes and revocations name the local client id, never the CLI's. HEY_TOKEN never stands in for it.
  • Device flow first. It sends POST /oauth/device_authorizations with client_id and scope=read write send, and no resource, per #9085's contract. It falls back to loopback authorization with the same scope and S256 PKCE on invalid_request (OAUTH_SCOPED_DEVICE_FLOW off), unauthorized_client (#9041 without #9085) or a 404. Other refusals, such as invalid_client, fail. The poll handles authorization_pending, slow_down (+5 s), access_denied, expired_token and invalid_scope, and sends install_id. The device code is never printed. The link and user code are sanitized before they're shown, and only an http(s) link is opened.
  • It reads the token response's scope. A narrower grant is stored as granted: with send unchecked, hey mcp serves no hey_send; without write, reads only. An omitted scope means what was requested (RFC 6749 §5.1). A grant naming anything not requested (full, which would mean the id is the CLI's own row) is refused and nothing is stored. An id equal to the CLI's is refused before HEY is contacted.
  • hey auth logout --mcp revokes the grant with DELETE /oauth/tokens (the local client id and the refresh token), then removes the local credential. If the revocation fails, the credential is still removed and a warning says to remove the app in Connected Apps.
  • hey auth status reports both credentials, in JSON under mcp and styled as an MCP: line. Neither form includes a token.
  • Client id. config.DefaultMCPClientID is vd96nBiRFXz1AjTXbADAtkrg, the id haystack#9215 pins in every environment. HEY_MCP_CLIENT_ID, then mcp_client_ids in the global config (keyed by origin), override it. A repo's .hey/config.json can't set it.

2. No fallback to the CLI credential

hey mcp never falls back, not even for reads:

  • The CLI credential holds full. --read-only is hey mcp's own rule, not HEY's. A read-only fallback still puts a full-scope token in the process the model drives, and the containment that justifies the local_mcp row (#9041's threat model) would then rest on hey-cli's gateway filter.
  • A fallback would leave the old path working, so people would never migrate.
  • The cost is one sign-in, and the refusal names it.

3. send_draft, written in hey-cli

Why it isn't imported. hey-mcp-server's server package is still under internal/ (hey-mcp-server#14, open), and hey-mcp-server is private while hey-cli is public, so requiring it breaks every outside build. The code is in internal/mcpserver (send.go, sendbody.go, sendprompt.go) so the W5.1 move can lift it as a unit.

How it's wired.

  • Its own tool. hey_send (D7) has destructiveHint and openWorldHint true. It's served only when the credential holds read and send (expanded, so write counts as read), the server isn't --read-only, and the threads domain is served.
  • Listed only where the person can be asked, as hey-mcp-server#30 ("One press per send") does: a receiving middleware drops hey_send from tools/list for a client that declares no form elicitation (read from the request's capabilities, so per-request _meta on 2026-07-28 and the session's initialize before it). A call from a cached list still sends nothing and answers with the draft's link (D3).
  • The guidance is one press per send. The hey_threads blurb, send_draft's description and each draft save's note say: the person can have a draft shown or revised as they like; when they ask to send, call send_draft at once, because its confirmation shows what goes out, so don't show the draft again or ask again. A draft save's note depends on whether this client is offered hey_send: if not, it names the draft's app_url and hey draft send <id>.
  • Two rounds over MRTR, through go-sdk v1.8.0's input requests. The first fetches GetMessageEdit and returns an elicitation whose HMAC-signed request state carries the draft's fingerprint, the sign-in, the origin, a 5-minute expiry and a method marker. The second verifies that state and the person's accept with confirm: true, re-fetches the draft and recomputes the fingerprint; any change fails with "the draft changed since you approved it". It then claims the draft and calls SendDraft once, through a client that neither retries nor refreshes-and-resends after a 401 (the token is renewed just before the claim instead). An answer with an id and no thread, where the draft's edit page is still served, is reported as not sent, as mail.ConfirmDelivered reads it. Every delivered field is written from the verified snapshot, and HEY replaces the whole recipient set when addressed is present. A 2026-07-28 client carries the rounds; an older one gets server-initiated elicitation/create from the SDK shim. Both are tested.
  • The fingerprint covers the draft id, sender id and address, sorted To/Cc/Bcc, the subject, the attachments (sgid, name, size, type, caption), a sha256 of the body as delivered (blob attachments rebuilt from their identity, so signed URLs don't count, and comments dropped), and mode now.
  • The prompt follows D2's layout: From, When, To, Cc and Bcc (Bcc included, "—" when empty) as "Name" <address>, the subject whole, attachments, images by target, then the body indented, links' targets beside their text, with at most 1,500 characters shown and the header saying how many there are. Controls, line separators and every invisible character are escaped as \u{…}. Whatever the text can't show (the rest of the body, what attachments hold, images, markup outside a text-and-links allowlist) is named on a Not shown here: line beside the draft's link in HEY. That line names only elements and attributes HTML knows, never a custom element's or attribute's name, which is the draft's own text.
  • Refusals: a draft HEY no longer serves for editing, one already scheduled (send_at isn't implemented), one with no recipients. Nothing a draft holds refuses it (sending-safety status, 2026-10-31).

Single use and never retried. The claim is per process, keyed by draft id, lasts 24 hours and is never released, even when HEY refuses. A replayed state, a second confirmation of the same draft and a resend after a failure are all refused with SendDraft called once, and a draft this process sent isn't put to the person again. Across processes, haystack #9163's draft lock delivers a draft once, which is why the hosted server's claim is per process too. The confirmation key is per process: both rounds reach the same hey mcp.

A process stays on the sign-in it started with. Its auth manager is pinned to that sign-in's key, so the tools it listed match the grant its token carries, and a later hey auth login --mcp is refused rather than adopted until restart. A renewal that narrows the grant below sending refuses the send before the claim.

Help and docs. hey mcp --help, hey auth login/logout --help, docs/agents.md ("Its own sign-in", "Drafts first, sends confirmed"), docs/cli.md, the environment help topic (HEY_MCP_CLIENT_ID), and the README's MCP line. .surface gains --mcp on login and logout. skills/hey/SKILL.md (the CLI's draft, review, send path from #615) is unchanged: the CLI has no server-side confirmation, so its in-chat review stays.

Tests

make check is green (fmt-check, vet, golangci-lint, unit tests, sync-skills, tidy-check, surface, release lockstep).

Asked for Tests
login stores a separate credential TestMCPLoginStoresACredentialApartFromTheCLIs, …InThePlaintextFallbackToo, TestAuthLoginMCPKeepsTheCLIsCredentialAndStatusShowsBoth
hey mcp refuses with no MCP credential TestMCPCommandRefusesWithoutItsOwnCredential (CLI signed in and HEY_TOKEN set; nothing reaches HEY), TestMCPManagerNeverUsesHEYToken
the scope response is honored TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor, TestMCPCommandServesWhatTheGrantAllows (with a client that can be asked, so hey_send's absence is the grant's doing), TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
send_draft needs a confirmed elicitation TestSendDraftSendsOnlyWhatThePersonConfirmed (both eras), TestSendDraftWithoutApprovalSendsNothing
listed only where it can be asked TestHeySendIsListedOnlyToClientsThatCanAsk (both eras; fails with the filter disabled), TestADraftSaveSaysHowThisClientSendsIt, TestSendDraftCantSendFromAHostWithoutElicitation (a cached list)
digest mismatch and replay refused TestSendDraftRefusesADraftChangedAfterApproval, TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange, TestAReplayedConfirmationSendsNothing, TestTwoConfirmationsOfOneDraftSendItOnce, TestSendDraftSendsADraftOnce, TestASentDraftIsntAskedAboutAgain, TestAForgedOrExpiredConfirmationSendsNothing
no refusal for content; the prompt names it TestNothingADraftHoldsRefusesIt (image, style, stylesheet, hidden text, non-blob and blob attachments), TestTheConfirmationShowsEveryImageAndLink, TestALongBodyIsShownInPartAndSaysHowMuch, TestThePromptsOwnLinesNameNoDraftText, TestAStylesheetsTextIsntShownAsTheBody

Also: inert rendering, whole subject and address, never retried and stays claimed, HEY keeping the draft, a sign-in changed or narrowed under the server, attachments delivered as their identity, comments never sent, and an end-to-end run through hey mcp (TestMCPCommandSendsAConfirmedDraftOnce, TestMCPCommandNeverResendsADeliveryAfterA401).

Not verified against a live HEY

  • Attachment sgid stability across two GetMessageEdit fetches. If it isn't stable, a draft with an attachment always fails as "changed", which fails closed. The same holds for an attachment that isn't a plain blob, which goes as it is and is hashed raw.
  • That UpdateMessage accepts the rebuilt, childless attachment figure as Trix's own submission.
  • The device flow against #9085 on staging, and loopback consent with send unchecked.

Still to do elsewhere

  • haystack: turn on OAUTH_SCOPED_DEVICE_FLOW per host (until then the login falls back to loopback).
  • hey-mcp-server: W5.1 (Fluent text input: positional args, short flags, and stdin #14) plus a way for a public module to depend on it; then these files move there and hey mcp imports them. Its stdio gateway refuses clearance writes other than screen-out (D5); hey-cli's still serves them, out of scope here.
  • hey-sdk: the behavior model still marks UpdateMessage idempotent with retries (D2.4); send_draft goes through hey-cli's no-retry client, as every hey mcp mutation does.

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 3f8aa2f. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 16 of the 16 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
attachments hashed with their signed URLs yes TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy
jeremy requested a balanced review from Copilot October 23, 2026 22:38
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T07:57:36.621912Z b6483ca New commits
🔒 Security Review ✅ Completed 2026-11-05T07:56:42.247730Z b6483ca New commits

Security findings

Advisory findings (6)

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread internal/mcpserver/server.go Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved confirmation-integrity, grant-bound exposure, and rolling recipient-limit gaps weaken the sending safeguards.

6 open issues
What changed in this PR

This PR gives hey mcp a separate, least-privilege sign-in and adds draft sending through explicit confirmation in the MCP client. Production rollout remains gated on provisioning the local MCP client ID.

Changes:

  • Adds isolated MCP credentials, device-flow login with browser fallback, revocation, and status reporting.
  • Introduces hey_send with draft-bound confirmation, persistent send claims, recipient limits, and exposure tracking.
  • Updates command help, documentation, and tests for the new authentication and sending behavior.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
README.md Updates MCP setup and sending guidance.
internal/​mcpserver/​server.go Registers sending and exposure tracking.
internal/​mcpserver/​sendstate.go Persists claims, limits, exposure, and confirmation keys.
internal/​mcpserver/​sendprompt.go Formats send confirmations and escapes fields.
internal/​mcpserver/​sendbody.go Analyzes bodies, attachments, and remote references.
internal/​mcpserver/​send.go Implements confirmed draft delivery and policy checks.
internal/​mcpserver/​send_test.go Tests confirmation, limits, exposure, and durability.
internal/​mcpserver/​exposure.go Classifies reads for exposure tracking.
internal/​mcpserver/​drafts.go Updates draft-only messaging to reference confirmed sending.
internal/​mcpserver/​domains.go Updates the threads tool description.
internal/​mcpserver/​dispatch.go Records exposure before returning read results.
internal/​config/​mcp.go Resolves the MCP client ID from trusted configuration.
internal/​config/​mcp_test.go Tests client ID resolution and validation.
internal/​cmd/​sdk.go Removes the superseded shared-client helper.
internal/​cmd/​root.go Initializes MCP authentication separately.
internal/​cmd/​mcp.go Uses MCP credentials and configures sending.
internal/​cmd/​mcp_test.go Tests credential isolation, tool availability, and sending.
internal/​cmd/​mcp_auth.go Implements MCP login, logout, and status handling.
internal/​cmd/​mcp_auth_test.go Tests MCP authentication commands.
internal/​cmd/​help_topics.go Documents MCP authentication environment settings.
internal/​cmd/​auth.go Adds MCP login/logout flags and status output.
internal/​auth/​store.go Stores scope, client, and grant metadata.
internal/​auth/​oauth.go Captures granted scopes in token responses.
internal/​auth/​mcp.go Defines isolated MCP authentication and scope validation.
internal/​auth/​mcp_test.go Tests scoped login, refresh, fallback, and revocation.
internal/​auth/​device.go Implements device authorization and polling.
internal/​auth/​auth.go Supports separate credential keys and scoped refreshes.
docs/​cli.md Documents MCP authentication commands.
docs/​agents.md Explains MCP sign-in and confirmed sending safeguards.
.surface Records MCP flags on login and logout commands.

Review effort: Balanced

Comment thread internal/cmd/mcp.go Resolved
Comment thread internal/mcpserver/send.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/mcpserver/sendprompt.go Resolved
Comment thread internal/mcpserver/server.go Resolved
Comment thread internal/mcpserver/sendstate.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f8aa2fc58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/config/mcp.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/mcpserver/server.go Resolved
Comment thread internal/cmd/mcp_auth.go Resolved
Comment thread internal/mcpserver/server.go Fixed
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 13c80ff, after round one's fixes. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 23 of the 23 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
attachments hashed with their signed URLs yes TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
body cap measured on the raw text yes TestEscapesCountTowardTheBodyThePromptCanShow
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 13c80ffa4f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread internal/auth/device.go Resolved
Comment thread internal/mcpserver/send.go Resolved
Comment thread internal/mcpserver/send.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13c80ffa4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/mcpserver/send.go Resolved
Comment thread internal/cmd/mcp.go Resolved
Comment thread internal/auth/device.go Resolved
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 47dc338, after round two's fixes. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 28 of the 28 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
attachments hashed with their signed URLs yes TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
body cap measured on the raw text yes TestEscapesCountTowardTheBodyThePromptCanShow
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
any preview image accepted yes TestAnAttachmentPreviewMustComeFromHEY
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread internal/mcpserver/send_test.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47dc338afd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/mcp.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/config/mcp.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 47dc338afd

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread internal/mcpserver/sendbody.go Resolved
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 495d641, after round three. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 29 of the 29 pieces fail their tests when they are taken out, and make check is green with them in. The attachment-preview row from the last proof is gone because that code is gone (attachments are rebuilt from their identity). The noscript guard has two independent halves, scripting-off parsing and the tag refusal, and either one alone holds it, so neither has a row of its own.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
body cap measured on the raw text yes TestEscapesCountTowardTheBodyThePromptCanShow
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows
attachment wrappers delivered as they came yes TestAnAttachmentIsDeliveredAsItsIdentityAlone, TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
a pinned manager adopts another sign-in yes TestAPinnedManagerRefusesAnotherSignIn
conflicting client ids for one origin accepted yes TestConflictingSpellingsOfOneOriginAreRefused

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy
jeremy requested a balanced review from Copilot October 23, 2026 23:31
…e trimming

- Check the sign-in again after the second round's review, renewing the token
  there, so a renewal that drops send refuses before the draft is claimed. The
  send itself no longer renews.
- Remove invisible characters from plain text before trimming trailing
  whitespace, so none can shield spaces from the trim.
- Name no draft text, such as an attachment's filename, in a refusal.
… drop comments

- A token that can't be renewed right before the claim refuses the send, so a
  draft that certainly wasn't sent isn't claimed.
- A write HEY applied is acknowledged as applied, with its answer withheld,
  when the exposure marker can't be written, so it isn't retried.
- The plain body keeps the spacing the confirmation showed (non-breaking
  spaces for leading and repeated spaces, and tabs).
- HTML comments are never sent; a conditional comment is markup Outlook
  renders.
…ts before the marker

- On the plain path, body and subject are NFKC-normalized after invisible
  characters go, so a no-break space and a decomposed letter go as what they
  look like. The subject sent is the one the confirmation showed.
- A write whose success is one exact status (rename_topic) is checked before
  the exposure marker, so a redirect is never reported as applied.
main's #599 classifies a refused Keychain read so the CLI stops asking for another
login. hey mcp's own credential check still wrapped it as a plain error, though hey mcp
is usually started by an MCP client's background process, the kind the Keychain
refuses.
A custom element's name is the draft's own text, and the refusal goes back to the
model without recording exposure. Name only elements HTML knows; an attribute is
already named by a key from a fixed list.
…d pin the client id

Sending-safety D4 (recipient caps, the known-recipient rule) and D6 (the
exposure window) were withdrawn on 2026-10-31: once a person grants send,
how much their agent sends and to whom is theirs to decide. With them go
the per-day caps, the 20-recipient cap, new-recipient flags, the exposure
table and markers, the plain-text path for new recipients, and the
refusals for content the prompt can't show. send_draft now refuses no
draft for what it holds; the confirmation shows images and links and names
whatever its text leaves out on a "Not shown here" line beside the draft's
link in HEY, as the hosted server does.

The send claim and confirmation key are per process, as hosted: the state
file, its lock and fsyncs existed for the caps and exposure, and HEY's
draft lock (haystack#9163) delivers a draft once across processes.

hey_send is listed only to a client that can answer an elicitation, and a
draft save tells the model to call send_draft as soon as the person asks
to send (its confirmation shows what goes out, so don't ask again), or,
for a client that can't be asked, names the draft's page in HEY.

DefaultMCPClientID is the id haystack#9215 pins for HEY MCP (local) in
every environment, so the unconfigured-id path is gone.
@jeremy
jeremy force-pushed the mcp-own-credential branch from cf1c83a to dd964be Compare November 5, 2026 06:09
@github-actions github-actions Bot removed the deps label Nov 5, 2026
@jeremy
jeremy marked this pull request as ready for review November 5, 2026 06:11
@jeremy
jeremy requested a review from a team as a code owner November 5, 2026 06:11
@jeremy
jeremy requested a balanced review from Copilot November 5, 2026 06:11
@jeremy

jeremy commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: dd964be6f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The styled auth status ignores credential-read errors, misreporting inaccessible keychain credentials as missing and recommending a sign-in that will not resolve the problem.

1 open issue

Review effort: Balanced

Comment thread internal/cmd/mcp_auth.go Resolved

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 29 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/cmd/mcp_auth_test.go Resolved
Comment thread docs/agents.md Resolved
Comment thread internal/mcpserver/send_test.go Resolved
Comment thread internal/cmd/mcp_test.go Resolved
Comment thread internal/config/mcp.go Resolved
Comment thread internal/cmd/mcp.go Resolved
Comment thread docs/agents.md Resolved
Comment thread internal/cmd/auth.go Resolved
Comment thread docs/agents.md Resolved
Comment thread internal/mcpserver/sendbody.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd992a7e9f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/mcp.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/auth/mcp.go Resolved
…it renders, and key the credential by canonical origin

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 172c1bcc0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/mcp.go Resolved
Comment thread internal/mcpserver/sendbody.go Resolved

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/mcpserver/sendbody.go Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6483ca3e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/mcpserver/sendbody.go Resolved
Comment thread internal/mcpserver/domains.go Resolved
@jeremy

jeremy commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Review threads: 26 resolved (19 fixed, 7 declined with the reasoning in each thread). CI green and mergeable on b6483ca; no open threads.
This round also reworked the PR per its brief: D4 and D6 removed (caps, known recipients, exposure, the plain-text path, content refusals, the send state file), hey_send listed only to clients that can be asked, one-press-per-send guidance (hey-mcp-server#30), DefaultMCPClientID = "vd96nBiRFXz1AjTXbADAtkrg" (haystack#9215), rebased onto #614 and #615.
Declined:

@jeremy
jeremy merged commit ed44b2a into main Nov 5, 2026
25 checks passed
@jeremy
jeremy deleted the mcp-own-credential branch November 5, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants