Skip to content

fix(agentex): put the connect link where the user is actually looking - #415

Closed
michael-chou359 wants to merge 1 commit into
mainfrom
mc/slack-link-dm-visibility
Closed

fix(agentex): put the connect link where the user is actually looking#415
michael-chou359 wants to merge 1 commit into
mainfrom
mc/slack-link-dm-visibility

Conversation

@michael-chou359

@michael-chou359 michael-chou359 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

What

The first real link offer in production was delivered correctly and reported as
never received
. Both were true:

  • chat.postMessage returned ok, and conversations.history confirmed the message
    sitting in the DM channel.
  • Slack files bot conversations under Apps, not in the Direct messages list — so
    "I've DM'd you a link" pointed at the one place it wasn't.

The connect link now goes in the ephemeral as well as the DM.

before                                  after
ephemeral: "I've DM'd you a link —      ephemeral: "<Connect your SGP account>
            check your DMs"                         …also sent to <our DM>"
DM:        <connect link>               DM:        <connect link>   (unchanged)
clicks to connect: 2, if you find it    clicks to connect: 1

Why this doesn't weaken anything

An ephemeral has exactly the same audience as a DM: Slack renders it for one user,
keeps it out of channel history, and out of search. The exposure argument that made
this DM-only never applied to an ephemeral — so routing someone through a conversation
they can't find, to click a link we could have handed them directly, bought nothing.

The DM stays because ephemerals are transient: reload Slack before clicking and
it's gone, and the offer cooldown would then block a retry for an hour. So the
ephemeral carries the link plus a deep link to the DM as the durable copy.

The invariant, stated properly

This change moves the line, so the rule is now written down precisely:

The nonce is a bearer token. It may go anywhere exactly one person can see it
(the user's DM, an ephemeral addressed to them) and nowhere that lands in channel
history
.

The test that asserted "never in a payload addressed to the origin channel" now
asserts "never in a chat.postMessage outside the user's own DM". That matters: the
old wording would have failed this change while the actual risk — a channel-visible
bearer token — was never touched by it. A test that encodes the implementation rather
than the property is one that blocks correct changes.

Also

  • conversations.open moved above the send-cap check, since both branches now need
    the channel id. It's idempotent (returns the existing DM rather than creating one).
  • Past the DM cap the user still gets the live link. The cap limits DMs, not what
    we're allowed to show the person in front of us.
  • Deep links use slack.com/app_redirect, not a slack:// URI, which fails on the
    web client.

Testing

3 new, 3 rewritten. The rewrites are the interesting ones — they invert assertions
that encoded the old design:

Was Now
nonce must not appear in the ephemeral nonce must appear in the ephemeral (single-viewer)
nothing addressed to the origin channel carries it nothing broadcast carries it
cap path posts only an ephemeral cap path opens the DM first, still no second DM

New: the ephemeral points at the durable copy; the cap path hands over the live link;
an unreachable DM offers nothing at all (we can't link to a conversation that doesn't
exist).

93 in the gateway suite, 689 across unit, ruff clean.

Not fixed here

The nonce is still 10 minutes. That was sized assuming prompt discovery, which this
incident showed was optimistic — but with the link now inline in the ephemeral, the
find-it delay largely disappears, so I'd rather see whether it's still a problem than
change two things at once.

🤖 Generated with Claude Code

Greptile Summary

The PR makes Slack account-link offers immediately discoverable by including the single-user connection link in the ephemeral response while retaining the DM as a durable copy.

  • Opens the user’s DM before evaluating the DM send cap so both response branches can construct a Slack deep link.
  • Adds the live account-link URL and DM redirect to normal and capped ephemeral responses.
  • Reworks unit tests around the single-viewer security invariant, capped sends, deep linking, and unreachable DMs.

Confidence Score: 5/5

The PR appears safe to merge, with the bearer nonce remaining confined to the user’s DM and explicitly user-targeted ephemeral messages.

The changed delivery flow preserves the single-viewer boundary, fails closed when a DM cannot be opened, and uses the same currently valid nonce for capped responses; no actionable changed-code defect remains.

Important Files Changed

Filename Overview
agentex/src/domain/use_cases/slack_gateway_use_case.py Adds the nonce URL and DM deep link to user-targeted ephemerals without introducing a channel-visible fallback or weakening the existing recipient boundary.
agentex/tests/unit/use_cases/test_slack_gateway_use_case.py Updates the security invariant and adds focused coverage for ephemeral link delivery, durable-copy redirects, send-cap behavior, and DM-open failures.

Reviews (1): Last reviewed commit: "fix(agentex): put the connect link where..." | Re-trigger Greptile

The first real link offer in production was delivered correctly and reported as
never received. Both were true: chat.postMessage returned ok, conversations.history
confirmed the message sitting in the DM channel -- and Slack files bot
conversations under "Apps", not in the Direct messages list, so "I've DM'd you a
link" pointed at the one place it wasn't.

The link now goes in the ephemeral as well as the DM.

An ephemeral has exactly the same audience as a DM: Slack renders it for one user,
keeps it out of channel history and out of search. So the exposure argument that
made this DM-only never applied to an ephemeral -- and routing someone through a
conversation they can't find, to click a link we could have handed them directly,
bought nothing.

The DM stays, because ephemerals are transient: reload Slack before clicking and
it's gone, and the offer cooldown would then block a retry for an hour. So the
ephemeral carries the link plus a deep link to the DM as the durable copy.

The security invariant is unchanged but is now stated precisely, because this
change moves the line: the nonce is a bearer token, so it may go anywhere exactly
one person can see it (the DM, an ephemeral) and nowhere that lands in channel
history. The test that used to assert "never in a payload addressed to the origin
channel" now asserts "never in a chat.postMessage outside the user's own DM",
which is the property that actually matters -- the old wording would have failed
this change while the real risk was untouched.

conversations.open moved above the send-cap check, since both branches now need
the channel id for the deep link. It's idempotent (returns the existing DM), and
past the cap the user still gets the live link -- the cap limits DMs, not what we
can show the person in front of us.

Deep links use slack.com/app_redirect rather than a slack:// URI, which doesn't
work on the web client.

Testing: 3 new tests plus 3 rewritten. The rewrites are the interesting ones --
they invert assertions that encoded the old design (nonce must not appear in the
ephemeral; nothing addressed to the origin channel may carry it) into the ones
that encode the new invariant. 93 in the gateway suite, 689 across unit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@michael-chou359
michael-chou359 requested a review from a team as a code owner August 30, 2026 05:42
@michael-chou359

Copy link
Copy Markdown
Contributor Author

Folded into #414 — same files, and the flag cut and this fix read better as one change.

@michael-chou359
michael-chou359 deleted the mc/slack-link-dm-visibility branch August 30, 2026 05:45
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