feat(rbac): Swift SPM capability-gating + denial handling (DEVA11Y-534)#28
feat(rbac): Swift SPM capability-gating + denial handling (DEVA11Y-534)#28Crash0v3rrid3 wants to merge 1 commit into
Conversation
ADR-0025 row 13: client capability-gating for the Swift SPM plugin. RBAC (DEVA11Y-518) is enforced server-side and surfaced by the headless browserstack-cli, which this plugin downloads and runs as a subprocess. All WebSocket work — auth, the connect/profile/handshake exchange that ships the `capabilities` set + `effectiveRole`, and capability-gating of lint/scan/set-config — lives in that CLI, not in this thin wrapper. The plugin therefore has no WebSocket message-decoding path or Codable response model of its own to gate on; capability decisions are read by the CLI from the server's capability set and never re-encoded here. The one RBAC signal the wrapper observes is the CLI's exit code. The CLI exits PERMISSION_DENIED (3) on a denied action (mirrors ExitCodes.PERMISSION_DENIED in accessibility-devtools-cli) and has already written the role-aware "Permission denied: …" detail to stderr. runCLI now detects exit code 3 and surfaces a clear, role-aware denial message instead of a generic failure, preserving the exit code so CI and the SPM build phase can distinguish a denial from a lint failure (1) or a tooling error (2). Generic exit-code forwarding is unchanged for every other status. Pre-RBAC CLIs (rollout gated by LINTER_RBAC_ENFORCEMENT_ENABLED on the server) never emit code 3, so the default path and backward compatibility are preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sunny-se
left a comment
There was a problem hiding this comment.
Code Review — Swift SPM Capability-Gating (DEVA11Y-534)
Reviewer: Sunny (via Claude Code)
Verdict: Approve ✅
Small, correct, well-scoped. +28/-0 in one file. Correct thin-wrapper approach — gates on CLI exit code 3 rather than duplicating capability logic. No security issues. No functional bugs.
What It Does
When browserstack-cli exits with code 3 (PERMISSION_DENIED), the SPM plugin surfaces a human-readable denial message via stderr before terminating, instead of silently forwarding an empty message. All actual RBAC logic stays in the CLI — this wrapper just interprets the exit code. Correct architectural boundary.
Highlights
- Exit code 3 preserved for CI pipelines (programmatic distinction: denial vs lint failure vs tooling error)
forwardExitreturnsNever— compiler knows theifblock terminates, no unreachable code- No force unwraps, static message string (no injection risk)
- Good inline comments explaining cross-repo coupling and why exit-code gating is correct for this repo
- No test infra exists in this repo (
Package.swiftdeclares only.plugintarget) — acceptable
Optional Suggestion
Consider extracting the denial message string into a named constant alongside browserstackCLIPermissionDeniedExitCode for consistency. Purely cosmetic — not worth a revision cycle.
🤖 Generated with Claude Code
Summary
Implements ADR-0025 row 13 — client capability-gating for the Swift SPM plugin — as part of the DevA11y RBAC rollout (DEVA11Y-518, tracked here under DEVA11Y-534).
Context: where RBAC actually lives for this client
This SPM plugin (
a11y-scan) is a thin wrapper: it downloads the headlessbrowserstack-cliand invokes it as a subprocess. All WebSocket work — authentication, the connect/profile/handshake exchange that ships thecapabilitiesset +effectiveRole, and the capability-gating of lint/scan/set-config — lives inside that CLI, which already implements RBAC (DEVA11Y-518). The plugin has no WebSocket message-decoding path and no Codable response model of its own to addcapabilities/effectiveRolefields to. Capability decisions are read by the CLI from the server's capability set and are never re-encoded in this repo.The single RBAC signal this wrapper observes from the CLI is its process exit code.
Change
runCLInow detects the CLI'sPERMISSION_DENIEDexit code (3, mirrorsExitCodes.PERMISSION_DENIEDinaccessibility-devtools-cli) and surfaces a clear, role-aware denial message instead of a generic failure. The CLI has already printed the specificPermission denied: …reason to stderr; the wrapper preserves the exit code so CI and the Xcode/SPM build phase can distinguish an RBAC denial from a lint failure (1) or a tooling error (2).scan/a11ypassthrough; gating of lint/scan/set-config is delegated to the CLI by design (it reads the server capability set). Documented inline so the constraint is explicit and no parallel policy matrix is introduced.Backward compatibility
Pre-RBAC CLIs never emit exit code
3, so the default path is unchanged. The rollout is gated server-side behindLINTER_RBAC_ENFORCEMENT_ENABLED; an absent capability set is treated as "allowed" by the CLI.Build note
swift buildwas attempted but the package declares only a.plugintarget (no buildable library/executable) —swift: The package does not contain a buildable target, which is expected for a command-plugin package. The source was syntax-checked withswiftc -parse(0 errors).Refs: DEVA11Y-534, ADR-0025 row 13, DEVA11Y-518.
🤖 Generated with Claude Code