fix(api): duplicate active rows for one linear workspace_slug are silently first-match-wins on remove-workspace — return 409 (N8, follow-up to #306)
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 38/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- aws, typescript
- Domain
- api, backend, cli, databases, documentation
Research direction
Read cdk/src/handlers/linear-remove-workspace.ts, especially the paginated lookup and MAX_SCAN_PAGES guard, then inspect the existing handler tests. Verify duplicate active slugs produce a 409 with colliding workspace IDs and no smSend call, while single- and zero-match behavior remains unchanged; also trace the CLI response types and LINEAR_SETUP_GUIDE.md error documentation.
Written by the indexing model from the issue text.
Description
Parent context: surfaced during PR #681 review by @isadeks (issue #306), review 5181793802 finding N8. Deferred from #681 deliberately because it changes API semantics; a bugfix PR is the wrong vehicle. Needs maintainer approved before implementation (ADR-003).
Finding
DELETE /v1/linear/workspaces/{slug} resolves the target row by scanning LinearWorkspaceRegistryTable with a FilterExpression on workspace_slug + status = 'active', then takes Items?.[0] and stops on the first match (cdk/src/handlers/linear-remove-workspace.ts, the do { … } while (!row && scanKey) lookup).
If two active rows share a workspace_slug, one is torn down and the other keeps status='active' and a live OAuth secret — and the caller is told the removal succeeded. The operator has no signal that a second live grant survives.
This is the same absent-vs-ambiguous conflation the rest of #681 was tightened to avoid: "I found a row" is being reported as "I found the row".
How duplicates arise
workspace_slug is the Linear urlKey, which is not immutable. A workspace renamed in Linear frees its old urlKey; a different workspace can then take it and be onboarded. Nothing in bgagent linear setup / add-workspace enforces uniqueness on workspace_slug — the table's key schema is on linear_workspace_id, so two distinct workspace ids may legitimately carry the same slug.
Note this is not reachable via the removal path itself: #681 added ConditionExpression: '#status = :active' to the revoke, so concurrent DELETEs cannot both succeed on the same row. N8 is about two different rows that share a slug.
Suggested fix (reviewer's wording)
Consider paginating to completion and returning 409 on a match count > 1.
Concretely:
- Continue the scan past the first match to the end of the keyspace (bounded by the existing
MAX_SCAN_PAGESguard added in #681) and collect all matches. - On
matches.length > 1, return 409 with a distinct error code (e.g.WORKSPACE_SLUG_AMBIGUOUS) whose body lists the collidinglinear_workspace_idvalues, so the operator can re-issue the removal against an unambiguous identifier. - Leave the single-match path byte-for-byte as-is.
Open design question worth settling in review: should the endpoint additionally accept linear_workspace_id as a disambiguator so a 409 is recoverable through the API rather than only through the manual runbook? Without it, the 409 is honest but leaves the operator in LINEAR_SETUP_GUIDE.md's manual fallback.
Why this is a semantics change, not a bugfix
Adding a 409 introduces a response the CLI and any other client must handle, and the paginate-to-completion change makes the lookup cost proportional to the whole table rather than to the position of the first match. Both belong behind their own review.
Acceptance criteria
- Lookup collects all
activematches for the slug, bounded byMAX_SCAN_PAGES. -
matches.length > 1→ 409 + distinct error code naming the colliding workspace ids; no revoke, noDeleteSecret, no row delete. - Single-match and zero-match behaviour unchanged (200 / 404).
- Handler test seeding two
activerows with oneworkspace_slugasserts the 409 and assertssmSendwas never called. - Error code documented wherever
WORKSPACE_NOT_FOUND/SECRET_DELETE_FAILEDare (docs/guides/LINEAR_SETUP_GUIDE.md), andcli/src/types.tsupdated if the response shape grows. - CLI surfaces the 409 with the colliding ids rather than a generic failure.
- Dominant language
- TypeScript
- Stars
- 146
- Forks
- 46
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 26
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from aws-samples/sample-autonomous-cloud-coding-agents
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug v1
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
bug v1
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
-
documentation P2 security
Difficulty 2/5 1-2 days Newbie friendliness 74/100
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
aws-samples/sample-autonomous-cloud-coding-agents#767 · 2 comments ·
All issues in aws-samples/sample-autonomous-cloud-coding-agents
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
vercel-labs/just-bash#464 ·
-
looksLikeSlug() is ASCII-only, so non-Latin entity slugs (e.g. Korean) skip exact match and collapse Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
TanStack/tanstack.com#1293 ·