Skip to content

emrg: an approval can be spent twice if the gate dies between the send and the ledger write #2449

Closed

Description

@how2how2how2-arch

What happens

scripts/feishu-gate.py marks an approval spent only after the CLI has accepted the
message: cmd_send calls send_message(...), and only then sets row["status"] = "sent"
and rewrites the ledger. If the process ends between those two points — a crash, a kill, a
host that sleeps through the CLI's 120 s timeout after the CLI already accepted — the row is
still pending.

The next send with the same token then passes every check the gate makes: the status is
pending, the body digest still matches, the request is inside --max-age, and the host's
reply still appears after the request in the stream. So the message goes out a second time.

Why that matters

"One approval, one message, one recipient" is the property the gate exists for, and it is
the property the tests arm in both directions. This path breaks it without any of those
tests noticing, because every test drives the gate through a CLI that answers.

Expected

An approval is spendable at most once, including across a crash of the gate itself.

What the fix must satisfy

  • The row must already be unspendable before the CLI is invoked, so that a crash leaves
    it in a state send refuses rather than one it accepts.
  • A genuine refusal from the CLI must not silently burn the approval. "Fail closed" and
    "keep the approval usable" are different choices with different costs, and whichever is
    taken must be stated in the script and pinned by a test — not decided by where a crash
    happens to land.
  • A test that ends the gate between the send and the ledger write, then runs send again,
    must observe a refusal and exactly one message in the stand-in CLI's stream. Testing
    only the second half would pass on a gate that refuses everything.
  • The Windows leg cannot run these tests (a stand-in CLI cannot carry the multi-line
    argument there), so the new test inherits the module's existing POSIX-only marker rather
    than inventing a second skip rule.

Scope

The window is narrow; this is a gap in a stated invariant, not a claim that the gate is
unsafe to use. The redundancy noted alongside it — the status == "sent" branch and the
status != "pending" branch below it both refuse, so removing either alone keeps the tests
green — is by design and needs no change.

Activity

  1. argszero commented on Oct 21, 2026

    @argszero
    Owner

    Handled by #2450

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions