Skip to content

ABAC resource policies for agents, LLM services, and MCP servers#886

Open
nickmisasi wants to merge 77 commits into
masterfrom
cursor/agents-abac-229e
Open

ABAC resource policies for agents, LLM services, and MCP servers#886
nickmisasi wants to merge 77 commits into
masterfrom
cursor/agents-abac-229e

Conversation

@nickmisasi

@nickmisasi nickmisasi commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Attribute-based access control for the three Agents resources, built on the new server plugin APIs (mattermost/mattermost#37509, mattermost/mattermost#37510, mattermost/enterprise#2231). The server owns policies (PAP), evaluation (PDP), and policy-existence resolution; this plugin is the enforcement point (PEP) — it stores no CEL, never evaluates locally, keeps no policy bookkeeping, and does not bundle cel-go.

Enforcement (JIT, no membership sync)

  • New accesscontrol package: Checker with CanUseAgent / CanUseService / CanUseMCPServer / ValidateAgentWrite, calling the server decision API with explicit allow/deny/no_policy/unavailable outcomes.
  • Agent user access gains a new Attribute-based level (alongside All/Allow/Block/None). In that mode the resource policy is the sole user-access gate; no_policy preserves legacy behavior (deliberate channel-style fail-open for attribute-based agents).
  • The server resolves policy existence even when ABAC evaluation is unavailable (open core / unlicensed / disabled), so unavailable and transport errors simply always fail closed — outages deny exactly the resources a policy gates, with no client-side policy index.
  • Agent + service use are re-checked on every completion (DM, mention, regen, tool follow-up, bridge, search) via the composite gate in bots.CheckUsageRestrictionsForUserConfig.
  • MCP: external servers get an immutable stable ID; unauthorized servers are filtered before tool registry/meta-tool construction (denied servers are never connected, and their auth errors never reach the tool store); search_tools/load_tool are omitted when no authorized catalog remains. Filtering is silent, server-level.
  • Agent/service/MCP list endpoints are filtered for UX; writes are independently validated (ValidateAgentWrite rejects unauthorized service/MCP assignment, checking only newly-assigned resources on update).
  • Server-version gate: on servers below 11.10 (which lack the ABAC plugin APIs) the plugin activates in an explicit legacy-only mode — legacy access levels behave as today, ABAC UI is hidden, and persisted attribute-based agents fail closed (deny) until the server is upgraded.

Authoring

  • All editor traffic goes browser → plugin HTTP → trusted type-scoped plugin PAP/CEL APIs with actingUserID (no browser-to-core calls). Agent policies: agent creators/admins + system admins; service/MCP policies: system admins only (System Console).
  • UI reuses the platform editors via window.Components (feature-detected): simplified table editor for agent creators, advanced CEL editor for system admins; inline policy sections on the agent Access tab and the System Console service/MCP panels. Resources with legacy (non-26-char) IDs show an explanatory note instead of an editor. A generated host-contract snapshot + CI probe guards the cross-repo prop contract against drift.

Migrations (one-time, activation, cluster-mutex + marker, single transaction)

  • Legacy 36-char UUID service IDs → 26-char Mattermost IDs across the active config row (services[].id, fallbackServiceID, config bots) and Agents_UserAgents.ServiceID (incl. soft-deleted; no DDL — 26 fits VARCHAR(36)). Future service IDs are server-minted. Note for support: restoring an old config-history row by hand also requires clearing the abac_service_id_migration_done marker.
  • External MCP servers get stable IDs, idempotently; admin config saves reconcile IDs in global phases and reject ambiguous/duplicate/fabricated identities with 409 rather than ever transferring a policy between servers.
  • All config read-modify-write paths now go through an atomic UpdateConfig; the write primitive itself rejects reintroduction of legacy UUID service IDs post-migration.

Not in scope (per spec): membership-sync jobs, per-tool MCP policies, share-time warnings, permission-policy scoping of manage_own_agent/manage_others_agent.

⚠️ Merge dependency: go.mod pins server/public to a pseudo-version of the open server PR branch (mattermost/mattermost#37509) so CI can build. Before merging, that PR must merge and the pin must be repointed to a master pseudo-version (commit subject marks it). min_server_version stays 11.9.0 — the version gate handles older servers explicitly. Playwright e2e specs for ABAC flows are deferred until a server image containing the new plugin APIs exists for testcontainers; an outline ships in e2e/tests/agents/access-control.spec.ts, and the existing access-control e2e suite passes against the released 11.9 image (legacy-only mode).

Screenshots

All demos recorded against a locally built server+enterprise (Enterprise Advanced license, ABAC enabled), with CPA attributes department=Engineering|Sales on two test users, a mock OpenAI-compatible LLM, and a real external MCP server.

demo1_author_agent_attribute_policy.mp4
Demo 1 — authoring an agent policy: Attribute-based access level, simple table editor, advanced CEL editor, test-rule modal, save + persistence.

demo2_agent_policy_enforcement_eng_allowed_sales_denied.mp4
Demo 2 — agent enforcement: Engineering user gets replies; Sales user has the agent filtered from lists and gets silent denial; an unrestricted agent still replies to Sales.

demo3_service_policy_review_and_per_completion_enforcement.mp4
Demo 3 — LLM service policy: admin-only console editor, legacy-ID note, and per-completion enforcement flipping the same agent between deny (Sales) and allow (Engineering).

demo4_mcp_policy_runtime_tool_filtering.mp4
Demo 4 — MCP server policy: console authoring and runtime tool filtering — Engineering sees the external server's tools, Sales sees it silently removed from the Tool Providers list.

Engineering: external MCP server present
Sales: external MCP server filtered out
Legacy-ID service note instead of policy editor

Release Note

Added attribute-based access control (ABAC) for agents, LLM services, and MCP servers on Mattermost Enterprise Advanced: a new Attribute-based user access level for agents with an inline policy editor, admin-only access policies on LLM services (enforced on every completion) and MCP servers (runtime tool filtering), a one-time migration of LLM service IDs from UUIDs to 26-character Mattermost IDs, and stable IDs for external MCP servers. Requires Mattermost Server 11.10+ with attribute-based access control enabled; on older servers the plugin runs with legacy access controls only and attribute-based agents are denied.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added attribute-based access control (ABAC) policy management for agents, services, and MCP servers, including CEL check/test tools.
    • Added an access policy editor to the system console (simplified + advanced modes) with expression validation, testing, and delete flows.
    • Introduced an ABAC availability status endpoint and stable identifiers for services and MCP servers to preserve policy assignments.
  • Bug Fixes

    • Search, model, agent, and MCP tool access now consistently enforces the configured permissions and hides tools for denied servers.
  • Documentation

    • Expanded the admin guide with ABAC concepts, lifecycle notes, and stable ID/migration details.

cursoragent and others added 30 commits July 14, 2026 20:34
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🤖 LLM Evaluation Results

OpenAI

⚠️ Overall: 22/28 tests passed (78.6%)

Provider Total Passed Failed Pass Rate
⚠️ OPENAI 28 22 6 78.6%

❌ Failed Evaluations

Show 6 failures

OPENAI

1. TestReactEval/[openai]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺) or heart/love emoji (e.g., ❤️).

2. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output does not provide an actual list of bugs. It explains lack of access and offers a template with two fill-in example entries, but it is not a compiled list of reported bugs.

3. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output does not actually include descriptions of each reported bug; it only asks the user to paste bug reports and provides a template with two illustrative fill-in examples rather than listing/describing the bugs in question.

4. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output provides a template and asks for bug reports, but it does not actually attribute any real bugs to users; the only user attributions shown are illustrative placeholders (@alice, @bob) in a fill-in example.

5. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention the specific bug about saving without a color or the save button doing nothing, and it does not attribute any bug to @maria.nunez. It only provides a generic template and asks for bug reports to be pasted.

6. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output does not mention the specific bug about an end user being able to change the channel banner, nor does it attribute any such bug to @maria.nunez. It only provides a generic template and example bugs attributed to @alice and @bob.

Anthropic

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ ANTHROPIC 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

ANTHROPIC

1. TestReactEval/[anthropic]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺) or a heart/love emoji (e.g., ❤️).

2. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output explains that it cannot access bug tracking systems and suggests where to look, but it does not actually provide a list of bugs. Therefore it does not satisfy the rubric requirement of being a list of bugs.

3. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output explicitly states it cannot retrieve or summarize reported bugs and provides suggestions on where to search; it does not include any actual bug descriptions.

4. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output states it cannot access bug trackers and suggests where to search, but it does not list any bugs or attribute any bug to a user.

5. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention @maria.nunez and does not attribute any specific bug (saving without a color; save button doing nothing) to anyone. It only states inability to access bug trackers and suggests where to search.

6. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output does not mention @maria.nunez or attribute the bug about an end user being able to change the channel banner to anyone. It only states lack of access and suggests where to search.

7. TestDirectMessageConversations/[anthropic]_bot_dm_tool_introspection

  • Score: 0.00
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to documentation (docs.mattermost.com) but does not mention GitHub anywhere, so it fails the requirement to both mention GitHub and refer to the documentation.

This comment was automatically generated by the eval CI pipeline.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds ABAC support for agents, services, and MCP servers, including policy APIs, runtime enforcement, stable resource IDs, migration safeguards, webapp policy editors, and editor-contract drift validation.

Changes

ABAC platform integration

Layer / File(s) Summary
Access decisions and policy proxy
accesscontrol/*, bots/permissions.go
Adds decision evaluation, policy operations, availability handling, legacy-only mode, and composite agent/service checks.
Stable IDs and atomic persistence
config/*, store/*, server/*
Adds stable service/MCP IDs, reconciliation, migrations, transactional updates, stale-ID safeguards, and activation wiring.
API and runtime enforcement
api/*, mcp/client_manager.go
Adds policy/CEL routes, agent and service enforcement, MCP filtering, and plugin-server admin-field updates.
Webapp policy authoring
webapp/src/client/*, webapp/src/components/*, webapp/src/types/*, webapp/src/utils/*
Adds policy clients, support detection, policy editors, attribute-based access controls, and stable-ID UI handling.
Editor contract validation
webapp/scripts/*, webapp/package.json, .github/workflows/ci.yml, Makefile
Adds host snapshots, compile-time probes, snapshot generation, and CI/style drift checks.
Supporting updates
llm/*, llmcontext/*, conversations/*, docs/*, telemetry/*, go.mod
Adds the attribute-based access level, context-aware checks, strict MCP catalog handling, documentation, telemetry attributes, and module updates.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: Setup Cloud Test Server

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: adding ABAC resource policies for agents, LLM services, and MCP servers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/agents-abac-229e

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
api/api.go (1)

175-251: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Make missing ABAC dependencies fail closed. The checker is optional at construction and the MCP listing also skips authorization when either the checker or stable resource ID is missing.

  • api/api.go#L175-L251: reject a nil accessChecker during API construction.
  • api/api_mcp.go#L94-L101: omit the server unless a stable ID exists and CanUseMCPServer succeeds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api.go` around lines 175 - 251, Make ABAC fail closed at both affected
sites: in api/api.go within New, reject a nil accessChecker during API
construction; in api/api_mcp.go within the MCP listing logic, include a server
only when it has a stable resource ID and CanUseMCPServer succeeds, omitting it
when either requirement is missing.
api/api_admin.go (1)

521-539: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate a committed configuration locally before reporting publication failure. Persistence succeeds before cluster notification, so returning early leaves the current node stale despite the committed database state.

  • api/api_admin.go#L521-L539: apply UpdatePluginServerAdminFields and configUpdater.Update immediately after UpdateConfig, before publishing.
  • api/api_admin_test.go#L876-L887: expect the local updater and runtime patch after a successful save even when publication fails.
  • api/api_admin_test.go#L951-L958: distinguish pre-commit failures from post-commit notification failures in the mutation assertions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api_admin.go` around lines 521 - 539, Move the local runtime patch via
UpdatePluginServerAdminFields and configUpdater.Update immediately after the
successful UpdateConfig persistence, before PublishConfigUpdate; retain the
publication error response afterward. In api/api_admin_test.go lines 876-887,
expect both local updates when publication fails, and in lines 951-958,
distinguish pre-commit failures from post-commit notification failures in
mutation assertions.
🧹 Nitpick comments (5)
e2e/tests/agents/access-control.spec.ts (1)

4-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use snake_case for the planned spec filenames.

Rename the listed files to abac_agent_access.spec.ts, abac_service_mcp.spec.ts, and abac_authoring_authz.spec.ts.

As per coding guidelines, **/*.{go,ts,tsx} files must use snake_case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/tests/agents/access-control.spec.ts` around lines 4 - 10, Rename the
planned spec files to snake_case: abac_agent_access.spec.ts,
abac_service_mcp.spec.ts, and abac_authoring_authz.spec.ts, updating any
references to their current kebab-case names while preserving their test
contents.

Source: Coding guidelines

api/api_search.go (1)

31-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated usage-gate + request-validation logic.

handleRunSearch and handleSearchQuery now both start with an identical CheckUsageRestrictionsForUser gate, on top of the pre-existing duplicated query/MaxResults validation block. Consider factoring the shared preamble (gate + decode + validate) into one helper returning (SearchRequest, error) (or a bool to abort), used by both handlers.

♻️ Sketch of a shared helper
func (a *API) prepareSearchRequest(c *gin.Context, userID string, bot *bots.Bot) (*SearchRequest, bool) {
	if err := a.bots.CheckUsageRestrictionsForUser(c.Request.Context(), bot, userID); err != nil {
		c.AbortWithError(http.StatusForbidden, err)
		return nil, false
	}
	if !a.searchService.Enabled() {
		c.AbortWithError(http.StatusBadRequest, fmt.Errorf("search functionality is not configured"))
		return nil, false
	}
	var req SearchRequest
	if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil {
		c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid request: %w", err))
		return nil, false
	}
	req.Query = strings.TrimSpace(req.Query)
	if req.Query == "" {
		c.AbortWithError(http.StatusBadRequest, fmt.Errorf("query cannot be empty"))
		return nil, false
	}
	if len(req.Query) > maxSearchQueryLength {
		c.AbortWithError(http.StatusBadRequest, fmt.Errorf("query exceeds maximum length of %d characters", maxSearchQueryLength))
		return nil, false
	}
	if req.MaxResults <= 0 {
		req.MaxResults = defaultMaxResults
	} else if req.MaxResults > maxMaxResults {
		req.MaxResults = maxMaxResults
	}
	return &req, true
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api_search.go` around lines 31 - 125, Extract the duplicated usage gate,
search-enabled check, request decoding, query validation, and MaxResults
normalization from handleRunSearch and handleSearchQuery into a shared
prepareSearchRequest helper. Have the helper abort the Gin context and report
failure consistently, return the prepared SearchRequest on success, and update
both handlers to use it while preserving their distinct RunSearch and
SearchQuery calls.
bots/bots.go (1)

78-92: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Enforce the documented non-nil accessChecker invariant.

MMBots dereferences m.accessChecker unconditionally in bots/permissions.go:

  • m.accessChecker.CanUseAgent(...)
  • m.accessChecker.CanUseService(...)

If New is ever called with nil, the panic happens later in a permission check instead of at construction time. Add an explicit nil check in New to fail fast.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/bots.go` around lines 78 - 92, Update New to explicitly reject a nil
accessChecker during construction, failing immediately before creating and
returning MMBots. Preserve the existing initialization flow for non-nil
accessChecker values and use the project’s established failure mechanism.
accesscontrol/reconciler.go (1)

41-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Potential key-collision in dedup/cooldown maps.

Concatenating resourceType + "/" + resourceID as a map key can collide across different (resourceType, resourceID) pairs if either component contains "/". Using the reconcileRequest struct directly as the map key sidesteps this entirely.

♻️ Proposed fix: use struct keys instead of string concatenation
-	mu      sync.Mutex
-	pending map[string]struct{}
-	lastRun map[string]time.Time
+	mu      sync.Mutex
+	pending map[reconcileRequest]struct{}
+	lastRun map[reconcileRequest]time.Time
@@
 func newReconciler(cooldown time.Duration) *reconciler {
 	return &reconciler{
 		ch:       make(chan reconcileRequest, reconcileQueueSize),
 		stop:     make(chan struct{}),
 		done:     make(chan struct{}),
 		cooldown: cooldown,
-		pending:  map[string]struct{}{},
-		lastRun:  map[string]time.Time{},
+		pending:  map[reconcileRequest]struct{}{},
+		lastRun:  map[reconcileRequest]time.Time{},
 	}
 }
@@
 func (r *reconciler) enqueue(resourceType, resourceID string) {
-	key := resourceType + "/" + resourceID
+	key := reconcileRequest{resourceType: resourceType, resourceID: resourceID}

Also applies to: 82-82, 130-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@accesscontrol/reconciler.go` around lines 41 - 44, Update the deduplication
and cooldown maps to use reconcileRequest directly as their key instead of
concatenating resourceType and resourceID with “/”. Replace all corresponding
key construction and lookups, including the additional occurrences, while
preserving the existing request values and map behavior.
webapp/src/components/system_console/mcp_servers.tsx (1)

358-364: 📐 Maintainability & Code Quality | 🔵 Trivial

Route the fallback display name through intl.formatMessage for i18n coverage.

Server ${serverIndex + 1} is a hardcoded English fallback feeding resourceDisplayName. intl is already in scope here; the sibling Service component uses an i18n-aware fallback (serviceTypeToDisplayName) for the equivalent case.

♻️ Proposed fix
             {config.id && (
                 <ConsolePolicySection
                     resourceType='mcp'
                     resourceId={config.id}
-                    resourceDisplayName={config.name || `Server ${serverIndex + 1}`}
+                    resourceDisplayName={config.name || intl.formatMessage({defaultMessage: 'Server {number}'}, {number: serverIndex + 1})}
                 />
             )}

As per coding guidelines, webapp/**/*.{ts,tsx}: All new user-facing strings must go through i18n extraction instead of being hardcoded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/system_console/mcp_servers.tsx` around lines 358 - 364,
Update the resourceDisplayName fallback in the MCP server ConsolePolicySection
usage to call intl.formatMessage with an extracted message for the server label,
while preserving config.name when available and the existing serverIndex-based
numbering.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@accesscontrol/checker.go`:
- Around line 81-89: Update the Checker constructor New to accept
context.Context as its first parameter, then derive lifecycleCtx with
context.WithCancel(ctx) instead of context.Background(). Thread this
caller-provided context through the related LLM call path while preserving
existing cancellation behavior.
- Around line 24-26: Route access-control errors through typed internal details
and localized API responses instead of exposing literal English. In
accesscontrol/checker.go lines 24-26 keep ErrABACUnavailable internal; at lines
388-390 and 420-422 use typed errors/details for service and MCP denials. In
api/api_agents.go lines 381-386, 521-526, and 728-733 map validation and
model-fetch errors to extracted i18n messages before abortAgentRequest, rather
than passing err.Error() directly.

In `@accesscontrol/index_rebuild.go`:
- Around line 169-195: Prevent canceled heals from performing reads after
waiting for mutationMu: in Checker.healDivergenceUnderLock, check ctx.Err()
immediately after acquiring the mutex and return false before calling index.Has
or policyMarkerTruth, preserving the existing cancellation logging behavior. In
accesscontrol/index_heal_test.go lines 507-535, add a scenario where the heal
waits on mutationMu, the context is canceled, and then the lock is released;
assert that no under-lock policy fetch occurs.

In `@api/api_access_control.go`:
- Around line 338-349: Cap request-body size for all CEL proxy routes before
JSON binding. Update bindCELExpressionRequest, used by handleCELCheck and
handleCELVisualAST, and the inline binding in handleCELTest to wrap the request
body with the same http.MaxBytesReader limit and handling pattern used by
bindPolicyBody, preserving the existing validation and bad-request responses.

In `@api/api_admin.go`:
- Around line 472-476: Update the nil persisted-config branch in the relevant
admin configuration handler to use the project’s i18n extraction mechanism for
the user-facing “no plugin configuration available” error, rather than
constructing a hardcoded string. Preserve the existing error return behavior and
localization conventions used by nearby HTTP error paths.
- Around line 465-520: Synchronize the plugin-owned fields used by the
UpdateConfig transform with the registration path so a re-registration between
the initial read and transform cannot persist stale Name, Path, or
ExposeExternal values. Update the logic around live and UpdateConfig to read or
reconcile the latest registered entry while preserving the admin-owned patch
behavior. Add a test that re-registers the plugin between the initial read and
transform and verifies the persisted fields use the newer registration.

In `@api/api_agents_test.go`:
- Around line 66-71: Update mockConfigStore.UpdateConfig to capture the result
of transform, assign the transformed config to m.cfg when no error occurs, and
return the persisted config and error. Preserve the existing getErr handling and
propagate transform errors without overwriting the stored configuration.

In `@api/api_config.go`:
- Around line 117-122: Update the conflict responses in the configuration
handler around the ErrStaleLegacyServiceIDs and preceding identity-conflict case
to use the project’s existing i18n translation mechanism instead of hardcoded
admin-facing messages. Ensure both 409 error messages are registered for
extraction and preserve their current retry guidance and wrapped error details.
- Around line 29-35: Preserve IDs for existing services during the API
configuration save flow: before normalizeAdminConfig, reconcile ID-less services
against prev using stable service identity, or reject them when no match exists,
instead of assigning model.NewId() unconditionally. Update the normalization
logic around the service loop and add coverage proving repeated saves of an
ID-less existing service retain its original ID and associated ABAC policy.

In `@api/api_test.go`:
- Around line 530-540: Update newPassthroughAccessChecker to accept testing.TB,
create the checker, and register checker.Close with t.Cleanup before returning
it. Update the createTestBots call sites in api/api_test.go:640-640 and
api/api_admin_test.go:55-55 to pass t to this cleanup-aware helper.

In `@api/api.go`:
- Line 175: Update the API construction path that initializes accessChecker to
reject a nil *accesscontrol.Checker immediately, returning the existing
constructor error type or startup failure mechanism. Ensure all construction
paths enforce this required dependency before authorization-sensitive handlers
can run, while preserving normal initialization for non-nil checkers.

In `@config/mcp_config.go`:
- Around line 249-277: Reject duplicate MCP runtime origins instead of assigning
a new policy identity: in config/mcp_config.go:249-277, detect an unclaimed
entry whose BaseURL matches an already-claimed origin and return
ErrMCPServerIDConflict; in config/mcp_config.go:115-129, enforce unique origins
rather than last-entry-wins selection; update config/mcp_config_test.go:137-146
to expect the duplicate-BaseURL conflict and config/mcp_config_test.go:68-77 to
stop treating last-entry-wins as valid ABAC behavior.

In `@conversations/test_helpers_test.go`:
- Around line 21-25: Ensure every test-created accesscontrol.Checker is closed
via test cleanup: update newPassthroughAccessChecker in
conversations/test_helpers_test.go, the corresponding helper in
bots/bots_test.go, and newTestChecker in accesscontrol/checker_test.go to accept
testing.TB and register checker.Close with t.Cleanup; also register
t.Cleanup(c.Close) at accesscontrol/checker_test.go lines 302, 390, 425, 460,
and 629.

In `@mcp/client_manager.go`:
- Around line 282-306: Update deniedExternalOrigins and its GetToolsForUser call
path to avoid repeating direct CanUseMCPServer PDP evaluations on every request,
using request-scoped caching or an available batched access-check mechanism
while preserving one decision per eligible server and the existing denial
filtering behavior.
- Around line 187-196: Synchronize access to ClientManager.config in ReInit and
the request-path helpers, including the server filtering flow shown here, so
reads cannot race with config replacement. Use the manager’s existing
synchronization mechanism if available, or publish and consistently read an
immutable config snapshot while preserving the current deniedOrigins filtering
behavior.

In `@server/legacy_bot_migration.go`:
- Around line 120-126: Update the legacy bot migration flow around the
st.UpdateConfig callback so it cannot clear a newer Bots list than the
dbCfg.Bots snapshot used for agent creation. Either hold the configuration lock
across migration and agent creation, or compare prev.Bots with dbCfg.Bots and
abort/retry when they differ; only clear Bots after confirming they still match.

In `@store/id_migrations.go`:
- Around line 172-176: Remove the early return on an empty idMap in the
migration flow so dangling UUID fallback, bot, and agent references are still
detected when no service IDs are remapped. Preserve the no-op behavior only
after all reference checks complete, and ensure the one-time marker is not
committed before those checks run.

In `@webapp/scripts/check_editor_contract.mjs`:
- Around line 142-160: Update the snapshot validation block guarded by
fs.existsSync(snapshotPath) to skip the pinned-snapshot probe when
--update-snapshot mode is active, allowing the later regeneration flow to run.
Keep the live-host probe and normal snapshot validation unchanged for non-update
invocations.

In `@webapp/scripts/editor_contract/probe.ts`:
- Around line 64-82: Update the searchUsers contract probes to compare the
callbacks’ awaited return types, not only their inner result aliases. In
webapp/scripts/editor_contract/probe.ts lines 64-82 and
webapp/scripts/editor_contract/probe_snapshot.ts lines 53-71, add bidirectional
Awaited<ReturnType<...>> checks for the host and mirror callbacks while
preserving the existing users/total comparisons.

In `@webapp/src/components/access_control/policy_editor.tsx`:
- Around line 130-133: Update the Promise.all flow in the policy editor around
getAccessControlFields so request failures are no longer converted to an empty
AccessControlPropertyField array. Propagate the failure into the component’s
error state and render the existing localized retry state, while preserving
normal rendering when fields load successfully.

---

Outside diff comments:
In `@api/api_admin.go`:
- Around line 521-539: Move the local runtime patch via
UpdatePluginServerAdminFields and configUpdater.Update immediately after the
successful UpdateConfig persistence, before PublishConfigUpdate; retain the
publication error response afterward. In api/api_admin_test.go lines 876-887,
expect both local updates when publication fails, and in lines 951-958,
distinguish pre-commit failures from post-commit notification failures in
mutation assertions.

In `@api/api.go`:
- Around line 175-251: Make ABAC fail closed at both affected sites: in
api/api.go within New, reject a nil accessChecker during API construction; in
api/api_mcp.go within the MCP listing logic, include a server only when it has a
stable resource ID and CanUseMCPServer succeeds, omitting it when either
requirement is missing.

---

Nitpick comments:
In `@accesscontrol/reconciler.go`:
- Around line 41-44: Update the deduplication and cooldown maps to use
reconcileRequest directly as their key instead of concatenating resourceType and
resourceID with “/”. Replace all corresponding key construction and lookups,
including the additional occurrences, while preserving the existing request
values and map behavior.

In `@api/api_search.go`:
- Around line 31-125: Extract the duplicated usage gate, search-enabled check,
request decoding, query validation, and MaxResults normalization from
handleRunSearch and handleSearchQuery into a shared prepareSearchRequest helper.
Have the helper abort the Gin context and report failure consistently, return
the prepared SearchRequest on success, and update both handlers to use it while
preserving their distinct RunSearch and SearchQuery calls.

In `@bots/bots.go`:
- Around line 78-92: Update New to explicitly reject a nil accessChecker during
construction, failing immediately before creating and returning MMBots. Preserve
the existing initialization flow for non-nil accessChecker values and use the
project’s established failure mechanism.

In `@e2e/tests/agents/access-control.spec.ts`:
- Around line 4-10: Rename the planned spec files to snake_case:
abac_agent_access.spec.ts, abac_service_mcp.spec.ts, and
abac_authoring_authz.spec.ts, updating any references to their current
kebab-case names while preserving their test contents.

In `@webapp/src/components/system_console/mcp_servers.tsx`:
- Around line 358-364: Update the resourceDisplayName fallback in the MCP server
ConsolePolicySection usage to call intl.formatMessage with an extracted message
for the server label, while preserving config.name when available and the
existing serverIndex-based numbering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: bf8c98df-8d34-42b8-9a12-218acbb96aea

📥 Commits

Reviewing files that changed from the base of the PR and between 8add6af and 89c43b4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (106)
  • .github/workflows/ci.yml
  • Makefile
  • accesscontrol/checker.go
  • accesscontrol/checker_test.go
  • accesscontrol/client.go
  • accesscontrol/doc.go
  • accesscontrol/index_heal_test.go
  • accesscontrol/index_rebuild.go
  • accesscontrol/kv_policy_index.go
  • accesscontrol/kv_policy_index_test.go
  • accesscontrol/logger.go
  • accesscontrol/outcome.go
  • accesscontrol/pap.go
  • accesscontrol/pap_test.go
  • accesscontrol/passthrough.go
  • accesscontrol/pdp_client.go
  • accesscontrol/pdp_client_test.go
  • accesscontrol/policy_index.go
  • accesscontrol/reconciler.go
  • api/api.go
  • api/api_access_control.go
  • api/api_access_control_test.go
  • api/api_admin.go
  • api/api_admin_test.go
  • api/api_agents.go
  • api/api_agents_test.go
  • api/api_channel.go
  • api/api_config.go
  • api/api_config_test.go
  • api/api_llm_bridge.go
  • api/api_mcp.go
  • api/api_post.go
  • api/api_search.go
  • api/api_search_test.go
  • api/api_test.go
  • bots/bots.go
  • bots/bots_test.go
  • bots/permissions.go
  • bots/permissions_test.go
  • config/legacy_migrations.go
  • config/legacy_migrations_test.go
  • config/mcp_config.go
  • config/mcp_config_test.go
  • conversations/agent_mention_reminder.go
  • conversations/agent_mention_reminder_test.go
  • conversations/ask_user_question_flow_test.go
  • conversations/conversations_test.go
  • conversations/direct_message_eval_test.go
  • conversations/dm_conversation_test.go
  • conversations/handle_messages.go
  • conversations/handle_messages_test.go
  • conversations/loaded_state_flow_test.go
  • conversations/test_helpers_test.go
  • docs/admin_guide.md
  • e2e/tests/agents/access-control.spec.ts
  • go.mod
  • llm/configuration.go
  • llm/configuration_test.go
  • llmcontext/llm_context.go
  • llmcontext/llm_context_strict_test.go
  • mcp/client_manager.go
  • mcp/client_manager_access_test.go
  • mcp/client_manager_test.go
  • mcp/testhelpers_test.go
  • mcpserver/test_helpers_test.go
  • plugin.json
  • server/abac_id_migrations.go
  • server/legacy_bot_migration.go
  • server/main.go
  • store/config.go
  • store/config_test.go
  • store/id_migrations.go
  • store/id_migrations_test.go
  • telemetry/attributes.go
  • webapp/package.json
  • webapp/scripts/check_editor_contract.mjs
  • webapp/scripts/editor_contract/generate_snapshot.mjs
  • webapp/scripts/editor_contract/probe.ts
  • webapp/scripts/editor_contract/probe_snapshot.ts
  • webapp/scripts/editor_contract/shims.d.ts
  • webapp/src/client.tsx
  • webapp/src/client/access_control.ts
  • webapp/src/components/access_control/console_policy_section.test.tsx
  • webapp/src/components/access_control/console_policy_section.tsx
  • webapp/src/components/access_control/policy_editor.test.tsx
  • webapp/src/components/access_control/policy_editor.tsx
  • webapp/src/components/agents/agent_config_view.test.tsx
  • webapp/src/components/agents/agent_config_view.tsx
  • webapp/src/components/agents/tabs/access_tab.test.tsx
  • webapp/src/components/agents/tabs/access_tab.tsx
  • webapp/src/components/system_console/bot.tsx
  • webapp/src/components/system_console/config.test.tsx
  • webapp/src/components/system_console/config.tsx
  • webapp/src/components/system_console/llm_access.tsx
  • webapp/src/components/system_console/mcp_servers.test.tsx
  • webapp/src/components/system_console/mcp_servers.tsx
  • webapp/src/components/system_console/service.test.tsx
  • webapp/src/components/system_console/service.tsx
  • webapp/src/components/system_console/services.test.tsx
  • webapp/src/components/system_console/services.tsx
  • webapp/src/i18n/en.json
  • webapp/src/types/access_control.ts
  • webapp/src/types/access_control_editors.ts
  • webapp/src/types/host_editor_contract.snapshot.d.ts
  • webapp/src/utils/access_control.test.tsx
  • webapp/src/utils/access_control.ts

Comment thread accesscontrol/checker.go
Comment on lines +24 to +26
// ErrABACUnavailable is returned by ValidateAgentWrite when an agent is being
// saved into attribute-based mode while ABAC is not available on the server.
var ErrABACUnavailable = errors.New("attribute-based access requires Attribute-Based Access Control to be licensed and enabled on this server")

@coderabbitai coderabbitai Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Route the new access-control responses through i18n.

These literal English errors are returned to clients through abortAgentRequest.

  • accesscontrol/checker.go#L24-L26: keep the sentinel internal and select a translated public message at the API boundary.
  • accesscontrol/checker.go#L388-L390: represent service denial with a typed error rather than public English text.
  • accesscontrol/checker.go#L420-L422: represent MCP denial with typed details for localized formatting.
  • api/api_agents.go#L381-L386: map create-validation errors to extracted i18n messages instead of exposing err.Error().
  • api/api_agents.go#L521-L526: apply the same mapping for updates.
  • api/api_agents.go#L728-L733: extract and translate the model-fetch denial message.

As per coding guidelines, all new user-facing strings must go through i18n extraction instead of being hardcoded.

📍 Affects 2 files
  • accesscontrol/checker.go#L24-L26 (this comment)
  • accesscontrol/checker.go#L388-L390
  • accesscontrol/checker.go#L420-L422
  • api/api_agents.go#L381-L386
  • api/api_agents.go#L521-L526
  • api/api_agents.go#L728-L733
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@accesscontrol/checker.go` around lines 24 - 26, Route access-control errors
through typed internal details and localized API responses instead of exposing
literal English. In accesscontrol/checker.go lines 24-26 keep ErrABACUnavailable
internal; at lines 388-390 and 420-422 use typed errors/details for service and
MCP denials. In api/api_agents.go lines 381-386, 521-526, and 728-733 map
validation and model-fetch errors to extracted i18n messages before
abortAgentRequest, rather than passing err.Error() directly.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Disagree — the repo's convention returns plain-English errors from the Go API layer (many pre-existing examples in api/); server-side i18n/en.json is hand-curated and not wired to these paths. Webapp-surfaced strings are localized.

@coderabbitai resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread accesscontrol/checker.go Outdated
Comment on lines +81 to +89
func New(client DecisionClient, papi plugin.API, index PolicyIndex, mcpIDsByOrigin func() map[string]string, mutationMutex sync.Locker, log Logger) *Checker {
if mcpIDsByOrigin == nil {
panic("accesscontrol: New requires a non-nil MCP server ID resolver (use NoMCPServerIDs)")
}
if mutationMutex == nil {
mutationMutex = &sync.Mutex{}
}
lifecycleCtx, lifecycleCancel := context.WithCancel(context.Background())
c := &Checker{

@coderabbitai coderabbitai Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Derive the checker lifecycle from a caller-provided context.

Line 88 detaches reconciliation from parent cancellation by using context.Background(). Accept ctx context.Context as the first New parameter and call context.WithCancel(ctx).

As per coding guidelines, production code must avoid context.Background() and thread context as the first parameter through the LLM call path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@accesscontrol/checker.go` around lines 81 - 89, Update the Checker
constructor New to accept context.Context as its first parameter, then derive
lifecycleCtx with context.WithCancel(ctx) instead of context.Background().
Thread this caller-provided context through the related LLM call path while
preserving existing cancellation behavior.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed by the Option B redesign (63555580): the checker no longer runs a background reconciler — accesscontrol.New spawns no goroutine and has no internal context.Background() lifecycle; decision calls thread the request-scoped ctx.

@coderabbitai resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread accesscontrol/index_rebuild.go Outdated
Comment on lines +169 to +195
func (c *Checker) healDivergenceUnderLock(ctx context.Context, ref rebuildRef) bool {
c.mutationMu.Lock()
defer c.mutationMu.Unlock()

has, err := c.index.Has(ref.resourceType, ref.resourceID)
if err != nil {
logWarn(c.log, "ABAC policy index heal could not re-read the policy index under the lock",
"resource_type", ref.resourceType, "resource_id", ref.resourceID, "error", err.Error())
return false
}
want, readable := c.policyMarkerTruth(ref.resourceID)
if !readable {
logDebug(c.log, "ABAC policy index heal could not re-read the policy under the lock; leaving the marker unchanged",
"resource_type", ref.resourceType, "resource_id", ref.resourceID)
return false
}
if has == want {
return true
}

// Last gate before mutating: a context canceled between the truth fetch
// and the write (plugin shutdown) must make the heal a no-op.
if ctx.Err() != nil {
logDebug(c.log, "ABAC policy index heal abandoned before its write: lifecycle context canceled",
"resource_type", ref.resourceType, "resource_id", ref.resourceID)
return false
}

@coderabbitai coderabbitai Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent new policy reads after lifecycle cancellation.

A heal cancelled while waiting for mutationMu can acquire the lock later and begin new index and plugin API reads.

  • accesscontrol/index_rebuild.go#L169-L195: check ctx.Err() immediately after acquiring mutationMu.
  • accesscontrol/index_heal_test.go#L507-L535: add the mutex-wait cancellation scenario and assert no under-lock policy fetch occurs.
📍 Affects 2 files
  • accesscontrol/index_rebuild.go#L169-L195 (this comment)
  • accesscontrol/index_heal_test.go#L507-L535
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@accesscontrol/index_rebuild.go` around lines 169 - 195, Prevent canceled
heals from performing reads after waiting for mutationMu: in
Checker.healDivergenceUnderLock, check ctx.Err() immediately after acquiring the
mutex and return false before calling index.Has or policyMarkerTruth, preserving
the existing cancellation logging behavior. In accesscontrol/index_heal_test.go
lines 507-535, add a scenario where the heal waits on mutationMu, the context is
canceled, and then the lock is released; assert that no under-lock policy fetch
occurs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No longer applicable — this file and the whole heal/rebuild machinery (including mutationMu) were deleted in 63555580; unavailable/error outcomes now simply deny.

@coderabbitai resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread api/api_access_control.go
Comment thread api/api_admin.go Outdated
Comment on lines +465 to +520
// The request's fields are applied to the freshest persisted entry inside
// the UpdateConfig transform, which runs atomically under the config
// advisory lock. Deriving the merged entry from the live snapshot read
// above would let two concurrent field updates overwrite each other: both
// would rebase onto the same stale in-memory state.
var updated mcp.PluginServerConfig
saved, err := a.configStore.UpdateConfig(func(prev *config.Config) (config.Config, error) {
if prev == nil {
// A nil persisted config must not be silently replaced by a
// zero-value baseline; doing so would clobber unrelated settings
// (services, bots, MCP flags) on the next save.
return config.Config{}, errors.New("no plugin configuration available")
}
// Clone to avoid mutating the store's cached pointer.
cfg := prev.Clone()

// Merge by PluginID against the persisted list rather than overwriting
// with the in-memory snapshot, which would silently drop entries for
// plugins that are persisted but currently inactive in memory.
merged := append([]mcp.PluginServerConfig(nil), cfg.MCP.PluginServers...)
mergedIdx := -1
for i := range merged {
if merged[i].PluginID == pluginID {
mergedIdx = i
break
}
}

existing, getErr := a.configStore.GetConfig()
if getErr != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("failed to load config for plugin-server save: %w", getErr))
return
}
if existing == nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("no plugin configuration available"))
return
}
// Clone to avoid mutating the store's cached pointer.
cfg := existing.Clone()

// Merge by PluginID against the persisted list rather than overwriting
// with the in-memory snapshot, which would silently drop entries for
// plugins that are persisted but currently inactive in memory.
merged := append([]mcp.PluginServerConfig(nil), cfg.MCP.PluginServers...)
mergedIdx := -1
for i := range merged {
if merged[i].PluginID == updated.PluginID {
mergedIdx = i
break
// Field ownership: the live registry entry is authoritative for the
// plugin-owned fields (Name, Path, ExposeExternal) — the source
// plugin may have re-registered with new values since the entry was
// last persisted, and adopting the persisted object wholesale would
// resurrect the stale ones. Only the admin-owned fields (Enabled,
// ToolConfigs) are carried over from the freshest persisted entry,
// with the request patch applied on top.
base := live
if mergedIdx >= 0 {
base.Enabled = merged[mergedIdx].Enabled
base.ToolConfigs = merged[mergedIdx].ToolConfigs
}
}
if mergedIdx >= 0 {
merged[mergedIdx] = updated
} else {
merged = append(merged, updated)
}
cfg.MCP.PluginServers = merged
if req.Enabled != nil {
base.Enabled = *req.Enabled
}
if req.ToolConfigs != nil {
base.ToolConfigs = *req.ToolConfigs
}
updated = base

if err := a.configStore.SaveConfig(*cfg); err != nil {
if mergedIdx >= 0 {
merged[mergedIdx] = base
} else {
merged = append(merged, base)
}
cfg.MCP.PluginServers = merged
return *cfg, nil
})

@coderabbitai coderabbitai Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Synchronize the plugin-owned snapshot with re-registration.

live is captured before UpdateConfig; a concurrent re-registration can therefore persist stale Name, Path, or ExposeExternal values even though the later runtime patch preserves the newer entry. Read these fields under synchronization shared with registration, or reconcile the persisted entry when the patched runtime result differs. Add a test that re-registers between the initial read and the transform.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api_admin.go` around lines 465 - 520, Synchronize the plugin-owned fields
used by the UpdateConfig transform with the registration path so a
re-registration between the initial read and transform cannot persist stale
Name, Path, or ExposeExternal values. Update the logic around live and
UpdateConfig to read or reconcile the latest registered entry while preserving
the admin-owned patch behavior. Add a test that re-registers the plugin between
the initial read and transform and verifies the persisted fields use the newer
registration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in b45f4951/7712c03f — updates now rebase on the freshest persisted entry inside the UpdateConfig transform; the live snapshot only supplies plugin-owned fields (Name, Path, ExposeExternal).

@coderabbitai resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread server/legacy_bot_migration.go
Comment thread store/id_migrations.go Outdated
Comment thread webapp/scripts/check_editor_contract.mjs Outdated
Comment thread webapp/scripts/editor_contract/probe.ts
Comment thread webapp/src/components/access_control/policy_editor.tsx
cursoragent and others added 2 commits July 15, 2026 16:47
…Option B)

The server now resolves policy existence during ABAC unavailability
(mattermost 4d1c1816fc): unavailable is returned only when a policy row
exists at the resource ID, else no_policy. That makes no_policy
trustworthy during outages, so the plugin-local fail-closed machinery is
deleted wholesale: KV policy index, cluster mutation mutex, activation
rebuild, decision-time reconciler, and the Checker lifecycle that
managed them.

- Amended decision tables: unavailable and call errors deny
  unconditionally, for all resources and all agent modes; no_policy
  keeps legacy behavior (incl. the deliberate attribute-based
  fail-open); the local invalid-ID -> no_policy short-circuit stays.
- IsAvailable redesigned to probe the PAP read path
  (GetAccessControlPolicy on a fresh ID; 404 => available) because the
  old decision probe reports no_policy whether ABAC is up or down under
  the new server semantics; 30s cache kept, userID param dropped.
- outcome.go deleted; model.AccessDecisionOutcome used directly, with
  unknown future values passed through to the checkers' deny default.
- SavePolicy/DeletePolicy reduced to single plugin-API calls.
- admin_guide.md: new outage semantics; lease-loss/self-healing notes
  removed.

Co-authored-by: nick.misasi <[email protected]>
Replaces the DEV-ONLY local-path replace with the pseudo-version of the
pushed mattermost cursor/agents-abac-platform-229e head (4d1c1816fc) so
plugin CI can build without a sibling checkout. TEMPORARY: must be
repointed to a master pseudo-version of server/public containing the
merged API before this PR merges.

Co-authored-by: nick.misasi <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
accesscontrol/pdp_client.go (1)

39-47: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail closed on a nil decision. If EvaluateAccessControl returns (nil, nil), decision.Outcome will panic here; return an error instead so callers deny access rather than crashing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@accesscontrol/pdp_client.go` around lines 39 - 47, Update the access-control
evaluation flow around EvaluateAccessControl to detect a nil decision when no
error is returned. Return a descriptive error before accessing decision.Outcome,
preserving the existing error recording and successful outcome handling so
callers fail closed and deny access.
server/main.go (1)

213-219: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don’t make the ID-migration broadcasts best-effort. The ABAC ID migration is already committed before these publishes run; if either send fails, peers can stay on stale config/agent state until another config change or restart, and there’s no retry path because the next activation will see idsMigrated == false. Persist a retryable notification or reload peers from the migrated state before returning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/main.go` around lines 213 - 219, Update the idsMigrated handling
around PublishConfigUpdate and PublishAgentUpdate so migration broadcasts are
not best-effort: when either publish fails, persist a retryable notification or
reload peers from the migrated state before the activation returns. Ensure the
committed ABAC ID migration cannot leave peers stale, and retain the existing
error context while making the failure recoverable.
🧹 Nitpick comments (1)
accesscontrol/pap_test.go (1)

95-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the combined not-found checks table-driven.

This test exercises two operations as separate cases. Represent Get and Delete through named table rows so future error-mapping cases remain consistent.

As per coding guidelines, Go tests must be table-driven when they contain more than one case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@accesscontrol/pap_test.go` around lines 95 - 112, Refactor
TestGetAndDeletePolicyNotFound into a named table-driven test with separate rows
for GetPolicy and DeletePolicy, capturing each operation’s setup, invocation,
and expected ErrPolicyNotFound result. Reuse shared IDs and API setup where
appropriate, and run the rows through a subtest loop while preserving the
existing not-found behavior and expectations.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/admin_guide.md`:
- Line 313: Update the ABAC engine availability sentence in the documented
policy behavior to add a comma after “for example,” while preserving the rest of
the wording and behavior description.

In `@go.mod`:
- Around line 236-241: Replace the dev-only pseudo-version in the server/public
replace directive with the released or master pseudo-version containing the
merged ABAC API. Update the dependency target for
github.com/mattermost/mattermost/server/public and remove the temporary
branch-pin state before merging; do not use a branch pin or local-path
replacement.

---

Outside diff comments:
In `@accesscontrol/pdp_client.go`:
- Around line 39-47: Update the access-control evaluation flow around
EvaluateAccessControl to detect a nil decision when no error is returned. Return
a descriptive error before accessing decision.Outcome, preserving the existing
error recording and successful outcome handling so callers fail closed and deny
access.

In `@server/main.go`:
- Around line 213-219: Update the idsMigrated handling around
PublishConfigUpdate and PublishAgentUpdate so migration broadcasts are not
best-effort: when either publish fails, persist a retryable notification or
reload peers from the migrated state before the activation returns. Ensure the
committed ABAC ID migration cannot leave peers stale, and retain the existing
error context while making the failure recoverable.

---

Nitpick comments:
In `@accesscontrol/pap_test.go`:
- Around line 95-112: Refactor TestGetAndDeletePolicyNotFound into a named
table-driven test with separate rows for GetPolicy and DeletePolicy, capturing
each operation’s setup, invocation, and expected ErrPolicyNotFound result. Reuse
shared IDs and API setup where appropriate, and run the rows through a subtest
loop while preserving the existing not-found behavior and expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 0a614996-37b8-4fa5-ad2f-9efe2e95d079

📥 Commits

Reviewing files that changed from the base of the PR and between 89c43b4 and 189207d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (20)
  • accesscontrol/checker.go
  • accesscontrol/checker_test.go
  • accesscontrol/client.go
  • accesscontrol/doc.go
  • accesscontrol/logger.go
  • accesscontrol/pap.go
  • accesscontrol/pap_test.go
  • accesscontrol/passthrough.go
  • accesscontrol/pdp_client.go
  • accesscontrol/pdp_client_test.go
  • api/api_access_control.go
  • api/api_access_control_test.go
  • api/api_test.go
  • bots/bots_test.go
  • bots/permissions_test.go
  • conversations/handle_messages_test.go
  • conversations/test_helpers_test.go
  • docs/admin_guide.md
  • go.mod
  • server/main.go
💤 Files with no reviewable changes (1)
  • accesscontrol/logger.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • accesscontrol/doc.go
  • accesscontrol/passthrough.go
  • conversations/handle_messages_test.go
  • conversations/test_helpers_test.go
  • bots/permissions_test.go
  • accesscontrol/client.go
  • bots/bots_test.go
  • api/api_access_control.go

Comment thread docs/admin_guide.md Outdated
Comment thread go.mod
Comment on lines +236 to +241
// DEV-ONLY: branch pin for the in-flight ABAC plugin API (EvaluateAccessControl + PAP/CEL
// methods + Option B existence-resolving semantics). Currently the pseudo-version of the
// mattermost branch cursor/agents-abac-platform-229e head (4d1c1816fc). Before merge: repoint
// to a MASTER pseudo-version of server/public containing the merged API — never merge a
// branch pin, and never merge a local-path replace.
replace github.com/mattermost/mattermost/server/public => github.com/mattermost/mattermost/server/public v0.4.4-0.20260715162756-4d1c1816fcff

@coderabbitai coderabbitai Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '220,250p' go.mod

Repository: mattermost/mattermost-plugin-agents

Length of output: 1567


🏁 Script executed:

python3 - <<'PY'
print("need evidence")
PY

Repository: mattermost/mattermost-plugin-agents

Length of output: 187


🏁 Script executed:

rg -n "server/public|ABAC|EvaluateAccessControl|PAP/CEL|Option B" go.mod . -g '!**/node_modules/**'

Repository: mattermost/mattermost-plugin-agents

Length of output: 50393


Replace the temporary server/public branch pin before merging. go.mod:236-241 still points at a branch-head pseudo-version, so this would merge a dev-only dependency instead of the released server/public revision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` around lines 236 - 241, Replace the dev-only pseudo-version in the
server/public replace directive with the released or master pseudo-version
containing the merged ABAC API. Update the dependency target for
github.com/mattermost/mattermost/server/public and remove the temporary
branch-pin state before merging; do not use a branch pin or local-path
replacement.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The server/public pseudo-version pin is deliberate while this PR consumes APIs from mattermost/mattermost#37509; it will be dropped once that merges.

@coderabbitai resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

…lure

The generated plugin RPC client logs and swallows transport errors for
(*T, *model.AppError) methods, returning (nil, nil). Two consumers
mishandled that:

- PluginAPIClient.EvaluateAccessRequest would dereference a nil
  decision; it now returns an explicit transport-failure error, which
  the checkers' error path turns into a deny.
- IsAvailable counted (nil, nil) as available; the probe now requires
  either the expected fresh-ID 404 or a non-nil policy, so a transport
  failure reads as unavailable.

Also sharpen the no_policy wording in doc.go and admin_guide.md: legacy
modes run the legacy allow/block checks, while attribute-based agents
deliberately fail open without them.

Co-authored-by: nick.misasi <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/admin_guide.md (1)

305-305: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the manage-others'-agents authoring permission consistently.

Line 321 grants this permission policy read/write/delete access, but Line 305 says only creators and agent admins can author policies. Clarify whether these users also receive the simplified editor access.

Also applies to: 317-321

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/admin_guide.md` at line 305, Update the Agents access-policy
documentation around the simplified and advanced editor permissions to
explicitly describe the `manage-others'-agents` permission consistently with the
policy read/write/delete access described nearby. Clarify whether users with
this permission can author policies through the simplified table editor, while
preserving the existing distinctions for agent creators, agent admins, and
system admins.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/admin_guide.md`:
- Line 305: Update the Agents access-policy documentation around the simplified
and advanced editor permissions to explicitly describe the
`manage-others'-agents` permission consistently with the policy
read/write/delete access described nearby. Clarify whether users with this
permission can author policies through the simplified table editor, while
preserving the existing distinctions for agent creators, agent admins, and
system admins.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 944b3189-1fc2-4cbb-b235-228741b71e94

📥 Commits

Reviewing files that changed from the base of the PR and between 189207d and efa1a32.

📒 Files selected for processing (6)
  • accesscontrol/checker.go
  • accesscontrol/checker_test.go
  • accesscontrol/doc.go
  • accesscontrol/pdp_client.go
  • accesscontrol/pdp_client_test.go
  • docs/admin_guide.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • accesscontrol/doc.go
  • accesscontrol/pdp_client_test.go
  • accesscontrol/pdp_client.go
  • accesscontrol/checker.go
  • accesscontrol/checker_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bots/permissions_test.go`:
- Around line 388-447: Convert TestCheckUsageRestrictionsDowngradedServer’s
three sequential scenarios into a []struct{} table with case names and per-case
test functions or inputs, then execute them through a single t.Run loop.
Preserve the existing setup, assertions, and coverage for attribute-based
denial, legacy-mode behavior, and unrestricted service/MCP access, following the
table-driven pattern used by TestLegacyOnlyModeDecisionTable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 5d4b84af-e881-48d4-b6e1-95a669ca7530

📥 Commits

Reviewing files that changed from the base of the PR and between efa1a32 and dd81928.

📒 Files selected for processing (11)
  • accesscontrol/checker.go
  • accesscontrol/doc.go
  • accesscontrol/passthrough.go
  • accesscontrol/version.go
  • accesscontrol/version_test.go
  • bots/bots.go
  • bots/permissions_test.go
  • docs/admin_guide.md
  • go.mod
  • server/main.go
  • webapp/scripts/check_editor_contract.mjs
🚧 Files skipped from review as they are similar to previous changes (6)
  • accesscontrol/passthrough.go
  • bots/bots.go
  • accesscontrol/doc.go
  • webapp/scripts/check_editor_contract.mjs
  • go.mod
  • docs/admin_guide.md

Comment thread bots/permissions_test.go
cursoragent and others added 13 commits July 16, 2026 18:24
@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@nickmisasi
nickmisasi marked this pull request as ready for review July 21, 2026 04:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e668803695

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bots/permissions.go
Comment on lines +124 to +125
if err := m.accessChecker.CanUseService(ctx, requestingUserID, cfg.ServiceID); err != nil {
return wrapDeny(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce policies on fallback services

When an allowed primary service has a policy-denied service in its fallback chain, this gate checks only cfg.ServiceID; bots.EnsureBots still attaches the complete fallback chain and Bifrost invokes it when the primary fails. Consequently, a user denied access to the fallback service can still send completions through it during a primary outage. Evaluate every service in the resolved fallback chain for the requesting user before permitting the completion.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34
if report.Migrated {
reloaded, reloadErr := st.GetConfig()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reload config when another node completes migration

During concurrent cluster activation, a node can load the pre-migration config before waiting on this mutex, then observe that another node already set the migration markers. In that case MigrateABACIDs returns report.Migrated == false, so this branch skips reloading and the node continues with legacy service IDs while agent rows in the database contain the remapped IDs; bots may fail service lookup and legacy IDs also short-circuit ABAC evaluation. Reload the active config after acquiring/running the migration regardless of which node performed the write.

Useful? React with 👍 / 👎.

Comment thread mcp/client_manager.go
Comment on lines +243 to +246
deniedOrigins := m.deniedExternalOrigins(ctx, userID)

// Get or create client for this user (connects to remote servers only)
userClient, initialErrors := m.getClientForUser(ctx, userID)
mcpErrors := cloneMCPErrors(initialErrors)
userClient, initialErrors := m.getClientForUser(ctx, userID, deniedOrigins)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebuild cached MCP clients when a server becomes allowed

If a user's first request occurs while an MCP server is denied, the newly cached UserClients is created without that server. After the policy changes to allow it, this call still reuses the same client; the final filtering can remove newly denied tools but cannot add tools from a server that was never connected. Active users can therefore remain unable to use the newly allowed server indefinitely because each request refreshes the client activity timestamp, unless they explicitly refresh tools or become idle long enough for eviction.

Useful? React with 👍 / 👎.

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