Files
Arvin Xu 1eacff997e 🔒 fix(acceptance): close the holes the open discussion opened (#19448)
* 🔒 fix(acceptance): close the holes the open discussion opened

Opening an acceptance's discussion to whoever holds its public link widened
three things it should not have.

An agent belongs to its workspace, not to its readers. The author-agent gate
was scoped by the acceptance's workspace, which answers "is this agent
visible there", not "may this caller use it" — and the discussion payload
hands a visitor the ids to pick from. It is now scoped to the workspace only
for callers who actually belong to it; everyone else falls back to their own
agents.

Read-only means read-only. A `viewer` of the acceptance's own workspace was
swept in by "anyone signed in who can read it" and could write on a private
delivery, unlike the same role on topic and document comments. Commenting
now needs the public link or a non-viewer seat.

An open discussion needs a way to close an unwanted remark. Its author is
exactly who will not remove it, so the acceptance's reviewers may now take
one down — the same tombstone the author's own deletion leaves — and a
visitor gets a per-minute ceiling on how much they can land.

Two client reads of `canDelete` as "I wrote this" are replaced by the author
id, since that flag now also turns on for a moderator: whether a thread may
be closed, and whose approval the reviewer bar is showing. The read-only
line no longer flashes while the permission answer is in flight, and the
sign-in return URL moves to a shared helper.

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

* 🔒 fix(acceptance): do not call an outside reader a departed teammate

The `former` tag infers "left the workspace" from "is not a member now".
That only holds on a delivery nobody outside could write on. With the link
public, most people in the discussion never had a seat, so the tag is
dropped there rather than guessed.

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

* 🐛 fix(acceptance): charge the visitor ceiling where the row is written

Codex review of the open-discussion hardening, three findings.

The ceiling was counted before the write and enforced after it, so a burst
of parallel requests each read the same under-limit count and all landed —
which is the shape a flood actually arrives in. It now travels into the
write: the transaction takes an advisory lock on (acceptance, author),
inserts, counts, and rolls itself back when it is the one over the line.
Everybody else still writes in parallel.

The same move fixes a retry being punished for a remark it already made. A
replay of the tenth comment's idempotency key inserts nothing, so it is no
longer counted and answers with the row it wrote the first time, instead of
a refusal for a lost response.

`canResolveThread` reads the signed-in identity, which arrives after the
first paint, but the overlay memo did not depend on it — a thread could keep
the answer computed while nobody was signed in until an unrelated comment
changed. It is a `useCallback` on what it reads, and the memo depends on it.

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

* 🐛 fix(workbench): let the SSR electron stub answer the gateway device question

Five shared store modules read getElectronStoreState().gatewayDeviceInfo to
decide whether a working directory is local. They enter the workbench SSR graph
through the acceptance check route, where the stub had no such key, so the stub
surface guard failed the build. Give the stub the key with no device, which is
the truthful answer on the server.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 10:15:06 +08:00
..
…