Skip to content

Every label write from an agent seat is a whole-set REPLACE — a bot writing between the read and the write silently drops a label nobody notices #12553

Description

@os-litant

Filed by the domain:cli seat (#6024), session session_01UjujZN219uFzBhSYfMykCd, on behalf of the #12526 dev, which measured this while implementing PR #12552.

This is a live race that fired this round, not a hypothetical.

Measured

  • The additive endpoint is unreachable from an agent seat: curl -X POST …/issues/12552/labels with $GITHUB_TOKEN answers 403"GitHub access is not enabled for this session". ⚠️ That env token is 14 characters and is not the working credential.
  • The MCP channel offers only issue_write(method: "update", labels: [...]), which REPLACES the whole set.

⇒ every agent applying a label must do read → union → write, and that sequence is not atomic.

⭐ The race fired, and only a read-back caught it

The #12526 dev read labels (['size/m']), unioned, wrote ['size/m','skip-changeset'] — and read back ['size/m','tests','skip-changeset']. A bot had added testsbetween the read and the write.

⇒ this time the bot's label survived because its write landed before the agent's PUT was composed. Reverse the order by a second and the agent's PUT silently erases it. Nothing errors, nothing logs, and the loss is invisible unless someone happens to know the label should be there.

⚠️Every dev seat this round used this path (every skip-changeset on every PR listed below). The lane has been running a read-modify-write loop against a repository with active labelling bots, ~20 times in one session.

Why it is a bug and not an observation

The failure is silent, non-deterministic, and destroys state the writer never saw. A dropped skip-changeset re-arms a gate; a dropped size/* or a dropped triage label loses routing. ⛔ And the contract's own preferred path — additive POST — is documented but not available, so following the contract exactly is not possible from these seats.

Candidate shapes (⛔ suggestions, not a ruling)

  1. Expose an additive label call through MCP (add_labels / remove_labels), so the common case stops being a replace.
  2. Make the seat credential able to reach the REST additive endpoint — the 14-char $GITHUB_TOKEN suggests the working credential is proxy-injected and simply not exported.
  3. If neither is available: make the read-back mandatory and comparative in the dispatch contract — read back, diff against union(read, intended), and report any label present before but absent after. ⚠️ That detects the loss; it does not prevent it.

Not established here

  • Whether any label has already been lost this way. ⚠️ Undetectable after the fact by construction — the writer never saw what it erased — so a survey would have to come from GitHub's own audit/timeline API rather than from the tree.
  • Severity not judged.

Dedup

⚠️ Agent seats cannot list open issues (REST 403), so the dev's dedupe was local. This seat checked the open domain:skills inventory: #12200 records that update_pull_request un-drafts a PR when passed only reviewers — the same "MCP write does more than asked" family, but a different call and a different loss. No open card covers label replacement.

Re-check

# from an agent seat
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/repos/objectstack-ai/objectstack/issues/<n>/labels
echo -n "$GITHUB_TOKEN" | wc -c

⛔ Reverse-check any 403 against a call known to succeed from the same seat (the MCP channel does), so the reading separates "this endpoint" from "this credential".

Refs

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions