Skip to content

fix(linear): resolve scope label names and stop blocking cycles/users - #47

Open
yosit wants to merge 2 commits into
Michaelliv:mainfrom
yosit:fix/linear-scope-label-resolution
Open

yosit wants to merge 2 commits into
Michaelliv:mainfrom
yosit:fix/linear-scope-label-resolution

Conversation

@yosit

@yosit yosit commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

A label-scoped Linear connection was unusable for an agent. Two independent faults, both in the linear plugin's scope layer.

1. A non-UUID scope value broke every issue query

The configured value went straight into labels: { id: { in: [...] } }, so a human-written LINEAR_SCOPE_LABEL_IDS=requester:yosi made Linear reject every issue query with Argument Validation Error: each value in in must be a UUID.

Scope values are now resolved before use:

  • UUIDs pass through untouched (no extra request at all)
  • anything else is looked up as a label name against a per-API-key directory — paginated, cached, re-read once before a name is declared unknown, and evicted from the cache when the fetch fails
  • an unresolvable value throws a message naming the value and the fix, instead of Linear's opaque validation error

2. Cycles and users were blocked outright

cycle, cycles, user and users were in SCOPED_BLOCKED_ROOT_FIELDS, so a scoped agent could not answer "what is assigned to me this cycle" at all — it bypassed the plugin and called Linear's GraphQL API directly with the same key, defeating the scope entirely.

The scope restricts issue content. Cycles and users are workspace metadata, like teams, states and labels, which were never blocked. So:

  • allowed: cycle.list/get, user.list/get
  • still blocked: cycle.create/update/archive, team.cyclesDeleteAll, user.update, and every previously blocked surface — comments, attachments, projects, milestones, project updates, views, initiatives, webhooks, org.get

The scope helpers became async as a result; call sites in issues.ts and views.ts follow.

Tests

Linear suite 37 → 48 tests, covering name resolution, cache reuse, the UUID fast path, the actionable unknown-label error, recovery when a label is created after the directory was cached, and the cycle/user read-vs-write split.

bun run test at the repo root: 497 pass, 0 fail. Biome clean; tsc --noEmit reports no errors under linear/.

A label-scoped Linear connection was unusable. Two independent faults:

1. The scope value went straight into `labels: { id: { in: [...] } }`, so
   a human-written value such as `requester:yosi` made Linear reject
   *every* issue query with "Argument Validation Error: each value in in
   must be a UUID". Scope values are now resolved: UUIDs pass through
   untouched, anything else is looked up as a label name against a
   per-API-key directory (paginated, cached, re-read once before a name
   is declared unknown, evicted on failure). An unresolvable value fails
   with a message naming the value and the fix.

2. `cycle`, `cycles`, `user` and `users` were blocked outright for scoped
   connections, so a scoped agent could not answer "what is assigned to
   me this cycle" at all — it bypassed the plugin and called Linear's
   GraphQL API directly. The scope restricts issue *content*; these are
   workspace metadata, like teams, states and labels, which were never
   blocked. Reads are allowed; cycle/user writes stay blocked, as do
   comments, attachments, projects, views, initiatives and webhooks.

The scope helpers became async as a result; call sites in issues.ts and
views.ts follow.
Descriptions are the entire disclosure surface: Vex's system prompt lists
only plugin and action *names*, so a description is all an agent sees when
it picks between neighbours in `actions.find` or the catalog.

- Every action a scoped connection cannot run now ends with "Unavailable
  on scoped connections." Previously only attachment.list and comment.list
  said so, and the other ~35 failed only at call time - a wasted turn each,
  and the exact trap that drove an agent off this plugin in SHFT-1644. The
  note is derived from the block set for list/get actions, so the copy
  cannot drift from the behaviour, and a new test asserts the invariant in
  both directions across all 90 actions.
- team.members is no longer blocked under scope. It is the same people
  surface as user.list, narrowed to one team.
- issue.list documents its convenience filters and the assignee/cycle/open
  filter shapes; cycle.list documents `{ isActive: { eq: true } }`. Those
  two are the "what is assigned to me this cycle" path.
- issue.listComments points at comment.list, which subsumes it; user.me
  says what it is for; issue.get gets its missing period.
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