feat(cli): bgagent linear remove-workspace + DELETE route + fail-closed resolver (#306) - #681
Open
scottschreckengaust wants to merge 3 commits into
Open
feat(cli): bgagent linear remove-workspace + DELETE route + fail-closed resolver (#306)#681scottschreckengaust wants to merge 3 commits into
scottschreckengaust wants to merge 3 commits into
Conversation
…ed resolver (#306) Add a `bgagent linear remove-workspace <slug>` command that deregisters a Linear workspace, replacing the manual DDB + Secrets Manager surgery that removal previously required. CLI (cli/src/commands/linear.ts): new subcommand mirroring add-workspace / update-webhook-secret UX — slug validation + a "type the slug to confirm" prompt (skipped by --yes). Delegates all writes to the backend via a new DELETE call so DDB/Secrets Manager grants stay on the API role, not on every CLI user (same pattern as `link`). Flags: --purge, --keep-mappings, --yes. Backend: new flat handler cdk/src/handlers/linear-remove-workspace.ts behind DELETE /v1/linear/workspaces/{slug} (route + Lambda wired in cdk/src/constructs/linear-integration.ts). Cognito-authenticated, admin-only (caller must match the recorded installed_by_platform_user_id). Default is a SOFT removal: flip the registry row to status=revoked (audit trail preserved) and delete the bgagent-linear-oauth-<slug> secret; --purge deletes the row outright. Secret deletion is idempotent (ResourceNotFoundException swallowed, other SM errors rethrown). Optional project-mapping cleanup keyed on linear_workspace_id. Fail-closed resolver: the OAuth resolver already rejects any non-active registry status (cdk/src/handlers/shared/linear-oauth-resolver.ts) — so a revoked workspace can no longer resolve a token or route webhooks the instant this returns. Added an adversarial test proving a revoked slug is rejected WITHOUT ever reading the secret, plus an active-control case. Docs: rewrote the "Removing a workspace" section of the Linear setup guide (Starlight mirror regenerated) to lead with the command and keep the manual DDB steps as a fallback. Security: no secrets logged (slug/workspace_id/booleans only); reuses existing AWS SDK clients; no new dependencies. SAST clean on new files. Closes #306 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… gaps Review follow-up (self-review agents on PR #681): - Secret-delete failure is no longer masked: a non-idempotent SM error (e.g. AccessDenied) after the row is revoked now returns a distinct 500 SECRET_DELETE_FAILED and writes a durable secret_deletion_failed / orphaned_oauth_secret_arn marker on the registry row, so the leaked credential is discoverable (a naive retry 404s at the active-scan and would never re-attempt the delete). ResourceNotFoundException stays idempotent (200). - Top-level catch + mapping-cleanup loop now log the failing phase + workspace id / per-page progress so on-call can locate an orphaned secret or half-cleaned mapping table from the request id. - Tests: SECRET_DELETE_FAILED path + marker write; FilterExpression pins the status='active' fail-closed filter to the handler (not the mock); paginated mapping cleanup across LastEvaluatedKey; missing oauth_secret_arn skip; CLI confirmation-prompt abort/proceed; secret_deleted:false CLI output; api-client query-string mapping (purge / keep_mappings snake_case). Relates to #306 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
✅ Acceptance summary (for the reviewer)#306 —
🔀 Merge guidance (for the reviewer)Cluster 🤖 orchestrator note (agent) — promotion is orchestrator-driven; merge remains a human action. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
bgagent linear remove-workspace <slug>command that deregisters a Linear workspace, replacing the manual DynamoDB + Secrets Manager surgery removal previously required. Ships the full flow: CLI subcommand, an authenticatedDELETE /v1/linear/workspaces/{slug}REST endpoint + Lambda handler, and a re-verified fail-closed OAuth resolver.Closes #306
Root cause / contract
There was no removal path. Onboarding is one command (
bgagent linear setup/add-workspace), but removal meant hand-editing theLinearWorkspaceRegistryTablerow, deleting thebgagent-linear-oauth-<slug>secret, and cleaning up project mappings — error-prone, undocumented, and prone to leaving dangling secrets or stale registry rows the resolver still reads.cli/src/commands/linear.ts(siblings mirrored:add-workspace,update-webhook-secret).cdk/src/handlers/linear-*.ts); routes are added incdk/src/constructs/linear-integration.ts.cdk/src/handlers/shared/linear-oauth-resolver.tsalready fail-closes on any registrystatus != 'active'(re-verified under test per the AC).The fix
CLI (
cli/src/commands/linear.ts,api-client.ts,types.ts): newremove-workspacesubcommand mirroring the sibling slug-validation + confirmation UX (type the slug to confirm; skipped by--yes). All destructive work is delegated to the backend viaApiClient.linearRemoveWorkspace()(DELETE), so DDB / Secrets Manager grants stay on the API role, not on every CLI user — same delegation pattern aslink. Flags:--purge,--keep-mappings,--yes.Handler (
cdk/src/handlers/linear-remove-workspace.ts, new, flat): Cognito-authenticated, admin-only (caller must match the recordedinstalled_by_platform_user_id). Default is a SOFT removal — flip the registry row tostatus='revoked'(audit trail preserved) and delete thebgagent-linear-oauth-<slug>secret.--purgedeletes the row outright. Secret deletion is idempotent (ResourceNotFoundExceptionswallowed, other SM errors rethrown). Optional project-mapping cleanup keyed onlinear_workspace_id.Route + IAM (
cdk/src/constructs/linear-integration.ts):DELETE /v1/linear/workspaces/{slug}behind the Cognito authorizer; new Lambda gets read/write on the registry + project-mapping tables andsecretsmanager:DeleteSecreton the documentedbgagent-linear-oauth-*prefix (same wildcard the webhook Lambdas already use). cdk-nag clean.Fail-closed resolver (
cdk/src/handlers/shared/linear-oauth-resolver.ts): already rejects any non-activestatus — so a revoked workspace stops resolving tokens and routing webhooks the instant this returns. Added an adversarial test.Support files (sanctioned by the respective hooks): added
WORKSPACE_NOT_FOUNDtocdk/src/handlers/shared/response.ts; addedLinearRemoveWorkspaceResponseto theCLI_ONLY_ALLOWLISTinscripts/check-types-sync.ts(it's a client-only response envelope, exactly like the existingLinearLinkResponse— CDK builds the body inline).Docs: rewrote the "Removing a workspace" section of
docs/guides/LINEAR_SETUP_GUIDE.mdto lead with the command and keep manual DDB steps as a fallback (Starlight mirror regenerated viamise //docs:sync).Testing
cli/test/commands/linear-remove-workspace.test.ts):--yesskips prompt + calls DELETE with default flags;--purge/--keep-mappingsforward the right query params; invalid slug rejected without hitting the API; API errors surface (not swallowed); mapping-removal count reported. 6/6 pass.cdk/test/handlers/linear-remove-workspace.test.ts): 401 (no JWT), 400 (bad slug), 404 (not found), 403 (not admin — no destructive calls), happy-path revoke + secret delete,--purgedeletes row, secret-already-gone idempotent, mapping cleanup,--keep-mappingsno-op, already-revoked → 404 (no re-revoke). 10/10 pass.cdk/test/handlers/shared/linear-oauth-resolver.test.ts): a revoked slug is REJECTED even with a fully valid non-expiring secret, and the secret is never fetched; active-control case still resolves. Plus the existingstatus != active → nullcoverage.cdk/test/constructs/linear-integration.test.ts): 4 Lambdas,workspaces/{slug}resources, DELETE method is Cognito-authorized, env wiring,DeleteSecretIAM.mise //cli:build(648 tests) pass,mise //cdk:buildtest phase (2548 tests) pass, cdk-nag clean for the new construct (verifiedRemoveWorkspace nag findings: []),security:sastexit 0,security:deps0 advisories,security:sast:maskingclean on my files.Security note
Fail-closed by design: a revoked or unknown slug is REJECTED, never resolved (404 also avoids the endpoint acting as a revoke-oracle). Admin-only removal; legacy rows missing
installed_by_platform_user_idcan only be removed via the documented manual fallback (fail-closed default). No secrets are logged — the handler logs only slug / workspace_id / request_id / booleans. Reuses existing AWS SDK clients; no new dependencies.Dependencies / related
cli/src/commands/linear.ts,cdk/src/constructs/linear-integration.ts, andcdk/src/handlers/shared/linear-oauth-resolver.ts. Edits here are localized (additive subcommand, additive route block) to keep the rebase mechanical — rebase ordering will be needed between this PR and feat(observability): solution attribution via native AWS_SDK_UA_APP_ID (#319, alt to #338) #345.Notes / follow-ups (not fixed here — out of scope)
LinearProjectMappingTablerows are keyed onlinear_project_idand carry no workspace identifier in the current schema (onboard-projectwrites onlyrepo/label_filter/team_id). Mapping cleanup therefore matches on alinear_workspace_idfield that today's rows don't have, so it's effectively a safe no-op until that field is recorded at onboard time. Documented in the guide; a schema addition (recordlinear_workspace_idon mapping rows) would make cleanup fully effective — candidate follow-up issue.ts-silent-success-maskingfindings oncli/src/commands/linear.ts:1765andcli/src/linear-oauth.ts:370are onmain, outside this diff.🤖 Generated with Claude Code
Self-review follow-up (looped
/review_pr, iteration 1)Ran
pr-review-toolkitagents (code-reviewer,silent-failure-hunter,pr-test-analyzer). Blocking findings addressed in follow-up commits:500 SECRET_DELETE_FAILEDand persists a durablesecret_deletion_failed/orphaned_oauth_secret_arnmarker on the registry row so the leak is discoverable.ResourceNotFoundExceptionstays idempotent (200).status='active'FilterExpression to the handler (not the mock); paginated mapping cleanup acrossLastEvaluatedKey; missing-oauth_secret_arnskip; the CLI confirmation-prompt abort/proceed branch (AC-required "prompts" surface);secret_deleted:falseCLI output; and the api-clientpurge/keep_mappingsquery-string mapping.Deferred nits (non-blocking):
BatchWriteCommandfor large mapping cleanups, and treating an SMInvalidRequestException"scheduled for deletion" as idempotent — candidate follow-ups, not needed for correctness here.