Skip to content

feat(mcp): profile-level read-only enforcement (KLA-409)#22

Merged
jklaassenjc merged 1 commit into
mainfrom
juergen/kla-409-profile-readonly
Apr 28, 2026
Merged

feat(mcp): profile-level read-only enforcement (KLA-409)#22
jklaassenjc merged 1 commit into
mainfrom
juergen/kla-409-profile-readonly

Conversation

@jklaassenjc
Copy link
Copy Markdown
Collaborator

@jklaassenjc jklaassenjc commented Apr 28, 2026

Summary

Closes KLA-409 — Slice C (the only remaining piece; allow/block lists and --read-only already shipped).

Adds a profile-level role field (auth_profile_role: read_only) so a profile bound to a read-only JumpCloud OAuth client can't accidentally be started in mutation-capable mode:

  • configProfileRole(profile), IsReadOnlyProfile(), SetProfileRole() with validation. Only ProfileRoleReadOnly ("read_only") is accepted; unknown values are rejected at set-time so typos don't silently fail to enforce anything.
  • jc mcp serve — new applyProfileRole() helper coerces readOnly = true when the active profile carries the role, emits a one-line stderr warning if the operator hadn't already passed --read-only, and rejects the start with a clear error if --read-only=false was passed explicitly.
  • jc auth status — surfaces a Profile Role line (and profile_role JSON field) when set; omits it otherwise to keep output clean for the 99% case.

Why

Today an operator can set up the right pattern (read-only OAuth client → dedicated jc profile → MCP-server use only) but still misconfigure the start command and end up advertising mutation tools that will only fail at the JumpCloud API layer with 403s. That's noisy, confusing in audit logs, and defeats the point of having a read-only credential. This adds a startup-time gate that closes the loop.

Behavior matrix

Profile role --read-only flag Result
not set unset / true / false unchanged (existing behavior)
read_only unset coerced to true; warning on stderr
read_only true (explicit) runs read-only; no warning (operator and profile agree)
read_only false (explicit) startup error with profile name + reason

Test plan

  • go build ./... clean
  • go test ./... — full suite passing
  • config: 4 new tests cover default-empty, read-only profile detection, SetProfileRole round-trip, invalid-role rejection
  • mcp: 4 new tests on applyProfileRole cover all branches of the matrix above
  • auth: 2 new tests cover the status output presence/absence of the Profile Role line

Follow-up

docs/AUTH.md (which lands in #21 / KLA-410) will get a small section on the profile role once that PR merges. This PR is code-only to avoid a stacked-PR conflict.

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes jc mcp serve startup behavior (new coercion/warnings and a hard error for incompatible flags) and introduces new config fields that affect runtime enforcement.

Overview
Adds a new profile-level role (profiles.<name>.auth_profile_role) with validation helpers (ProfileRole, IsReadOnlyProfile, SetProfileRole) to support a read_only profile mode.

Updates jc mcp serve to enforce read-only profiles via applyProfileRole(): it forces --read-only on when the active profile is read-only (emitting a stderr warning if not explicitly set) and fails fast if the operator explicitly passes --read-only=false.

Extends jc auth status to include the profile role in both human output and JSON (profile_role), omitting it when unset; adds targeted unit tests for the new status output, role helpers, and enforcement matrix.

Reviewed by Cursor Bugbot for commit 9a3dc17. Bugbot is set up for automated code reviews on this repo. Configure here.

Closes the misconfiguration vector that --read-only alone leaves open:
an operator who provisioned a JumpCloud OAuth client with read-only API
scope and bound it 1:1 to a jc profile could still accidentally start
'jc mcp serve' against that profile *without* --read-only and have
mutation tools advertised in ListTools. Calls would then fail noisily
at the JumpCloud API layer with 403s, polluting the audit trail.

Adds a profile-level role field (auth_profile_role: read_only) that
the MCP server honors at startup:

- config: ProfileRole(profile), IsReadOnlyProfile(), SetProfileRole
  with validation. Only ProfileRoleReadOnly ("read_only") is accepted.
- mcp serve: applyProfileRole() helper coerces readOnly=true when the
  active profile carries the role; emits a one-line stderr warning if
  the operator hadn't already passed --read-only; and rejects the start
  with a clear error if --read-only=false was passed explicitly.
- jc auth status: surfaces a 'Profile Role' line (and JSON field) when
  the active profile has a role set.

Note: KLA-409's original Parts A (allow/block lists) and B (--read-only
flag) were already shipped in earlier work; this is just the remaining
Slice C (profile-level safety net).

Tests: 4 new config tests, 4 new applyProfileRole helper tests, 2 new
auth status output tests. All package suites green.

Doc update for docs/AUTH.md will follow once PR #21 (KLA-410) lands;
this PR is code-only to avoid a stacked-PR conflict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jklaassenjc jklaassenjc merged commit baf7b95 into main Apr 28, 2026
6 of 7 checks passed
@jklaassenjc jklaassenjc deleted the juergen/kla-409-profile-readonly branch April 28, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants