Skip to content

feat(rbac): Swift SPM capability-gating + denial handling (DEVA11Y-534)#28

Draft
Crash0v3rrid3 wants to merge 1 commit into
mainfrom
feature/DEVA11Y-534-rbac-client-gating
Draft

feat(rbac): Swift SPM capability-gating + denial handling (DEVA11Y-534)#28
Crash0v3rrid3 wants to merge 1 commit into
mainfrom
feature/DEVA11Y-534-rbac-client-gating

Conversation

@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator

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 headless browserstack-cli and invokes it as a subprocess. All WebSocket work — authentication, the connect/profile/handshake exchange that ships the capabilities set + 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 add capabilities/effectiveRole fields 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

  • Denial handling: runCLI now detects the CLI's PERMISSION_DENIED exit code (3, mirrors ExitCodes.PERMISSION_DENIED in accessibility-devtools-cli) and surfaces a clear, role-aware denial message instead of a generic failure. The CLI has already printed the specific Permission 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).
  • Generic handling intact: every other non-zero status is forwarded exactly as before.
  • CTA gating: the plugin's only entry point is the scan/a11y passthrough; 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 behind LINTER_RBAC_ENFORCEMENT_ENABLED; an absent capability set is treated as "allowed" by the CLI.

Build note

swift build was attempted but the package declares only a .plugin target (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 with swiftc -parse (0 errors).

Refs: DEVA11Y-534, ADR-0025 row 13, DEVA11Y-518.

🤖 Generated with Claude Code

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 sunny-se left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
  • forwardExit returns Never — compiler knows the if block 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.swift declares only .plugin target) — 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

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.

2 participants