Repository navigation
Finding #1: require agent.manage capability for agent-provider writes - #149
Merged
Merged
Conversation
Tenant integration review follow-up (post-H3/M2/P2/PR#148): upsert_agent_provider, delete_agent_provider, backfill_agent_provider_key_metadata (MCP), register_plugin_binding's "agent" path, and REST PUT /api/plugin-bindings had no capability/authorization check at all - any authenticated caller, however narrowly scoped, could write, delete, or repoint any tenant's BYO LLM provider. Two related gaps in the same surface are fixed alongside it: - Changing only `endpoint` kept the stored BYO API key unchanged, silently sending it (as a Bearer token) to whatever new host was saved. AgentProviderConfiguration.Merge now clears the stored key/fingerprint/timestamp when the endpoint's host actually changes without a new key also being supplied; a path- or scheme-only edit to the same host is untouched. - Endpoint URLs were only checked for shape (any scheme, including bare http://, was accepted in every environment). All four write surfaces now require https:// outside Development (TenantIdentityRules.AllowInsecureAgentEndpoints, overridable via FlowOS:Agents:AllowInsecureEndpoints, mirroring the existing DemoKeysAllowed idiom). New capability agent.manage (AgentProviderKinds.ManageCapability) is added to the reserved Admin role's default capabilities only - TenantSecurityProvisioningService backfills it onto every tenant automatically on next deploy, so no migration is needed - and as an opt-in API-key scope (agent:manage) for tenants who want to grant it to a narrower role later. A shared TenantCapabilityGate.HasCapabilityAsync helper is extracted from the existing TenantTeamMcpTools/TenantIamMcpTools pattern so the MCP tools and the REST controller reuse one check instead of three copies. ICurrentUser/ICapabilityService are added as optional trailing constructor parameters on AgentContextMcpTools/PluginBindingMcpTools (matching this project's existing optional-param convention) to avoid breaking ~20 pre-existing 2/3-arg test constructions; the gate is skipped only when both are null, which happens only in those direct unit-test constructions - production DI always supplies both. PluginBindingsController's new dependencies (ICapabilityService, IWebHostEnvironment, IConfiguration) are required constructor params, since ASP.NET Core always supplies them via DI. Tests: new AgentProviderConfigurationTests (Merge host-change-clears-key behavior), new capability-gate and https-enforcement tests in AgentContextMcpToolsTests and PluginBindingMcpToolsTests, and a new PluginBindingsControllerTests for the REST path. Confirmed no pre-existing test in this codebase supplies a plain http:// agent-provider endpoint, so none break under the new default-to-strict scheme check. Docs: docs/08-security-roles-and-policies.md gets agent.manage in the capability-codes list; docs/07-ai-agents-and-insights.md and FlowOsMcpGuidance.cs describe the capability requirement, the https rule, and the endpoint-host-change key-clearing behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5is5Q7EFV6kqdnPTsFsM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Tenant integration review follow-up (finding #1, discovered alongside #2 which shipped as PR #148):
upsert_agent_provider,delete_agent_provider,backfill_agent_provider_key_metadata(MCP) and RESTPUT /api/plugin-bindingshad no capability/authorization check at all — any authenticated caller, however narrowly scoped, could write, delete, or repoint any tenant's BYO LLM provider. Two related gaps on the same write surface are closed in the same change, since they're part of the same exposure:endpointkept the stored BYO API key unchanged, silently sending it (as a Bearer token) to whatever new host was saved.http://. Endpoint URLs were only checked for shape — any scheme, including a barehttp://, was accepted in every environment, exposing the key on the network path.What
agent.manage(AgentProviderKinds.ManageCapability), granted to the reservedAdminrole's default capabilities only (notApiKeyCapabilities), plus an opt-in API-key scope mappingagent:managefor tenants who want to grant it to a narrower role later viagrant_role_capability.TenantSecurityProvisioningServicebackfills newAdminCapabilitiesonto every tenant automatically on the next deploy — no migration needed, and every realistic current caller (dashboard Admin, full-access API key, demo key, stdio/worker default) already resolves toAdmin, so nothing breaks; only a narrowly-scoped non-full-access API key is newly blocked, which is the point.upsert_agent_provider,delete_agent_provider,backfill_agent_provider_key_metadata(AgentContextMcpTools),register_plugin_binding'sagentpath (PluginBindingMcpTools), and RESTPUT /api/plugin-bindings's agent branch (PluginBindingsController). A denied caller getsMCP-AUTHZ-001(MCP) or403 Forbidden(REST).TenantCapabilityGate.HasCapabilityAsynchelper, extracted from the existingTenantTeamMcpTools/TenantIamMcpToolspattern, so all three call sites share one check instead of three copies.AgentContextMcpTools/PluginBindingMcpToolstakeICurrentUser/ICapabilityServiceas optional trailing constructor params (matching this project's existing optional-param convention fortoolHost/configuration) — the gate is skipped only when both are null, which happens only in ~20 pre-existing direct unit-test constructions; production DI always supplies both viaToolRegistration'sscope.ServiceProvider.GetRequiredService<T>().PluginBindingsControllertakesICapabilityService/IWebHostEnvironment/IConfigurationas required params since ASP.NET Core always supplies them.AgentProviderConfiguration.Merge()now clearsApiKey/ApiKeyFingerprint/ApiKeyUpdatedAtUtcwhen the caller supplies a newEndpointbut not a newApiKey, and the new endpoint's host differs from the previous one (Uri.Authoritycomparison, case-insensitive). A path- or scheme-only edit to the same host leaves the key untouched; re-supplyapiKeyin the same call when repointing to a new host.https://required outside Development. NewTenantIdentityRules.AllowInsecureAgentEndpoints(environmentName, configuredValue)(mirrors the existingDemoKeysAllowedidiom exactly) — explicit override viaFlowOS:Agents:AllowInsecureEndpoints, elsetrueonly inDevelopment. Enforced identically (duplicated per-class, since each resolves environment from a different source) inAgentContextMcpTools,PluginBindingMcpTools, andPluginBindingsController.register_plugin_binding's agent path also gained the endpoint shape+scheme validation it was previously missing entirely (unlikeupsert_agent_provider, which already validated shape).Tests
AgentProviderConfigurationTests(7 cases): host-changed-without-new-key clears the key; path-only, scheme-only, no-previous-endpoint, and new-key-supplied cases keep/replace it correctly; unparseable new endpoint is treated as a host change.AgentContextMcpToolsTests(6 cases: deny/allow on upsert, deny-before-delete, deny on backfill, http rejected in Production, http allowed in Development) andPluginBindingMcpToolsTests(4 cases: deny/allow on register_plugin_binding's agent path, non-URL endpoint rejected, http rejected outside Development).PluginBindingsControllerTests(5 cases) for the REST path: forbidden without capability, success with it, non-URL endpoint rejected, http rejected in Production, http allowed in Development.tests/) no pre-existing test in this codebase supplies a plainhttp://agent-provider endpoint, so the new default-to-strict scheme check breaks nothing already passing.Docs
docs/08-security-roles-and-policies.md:agent.manageadded to the "Recommended capability codes" list.docs/07-ai-agents-and-insights.mdandFlowOsMcpGuidance.cs: describe the capability requirement, thehttps://-outside-Development rule, and the endpoint-host-change key-clearing behavior.Not in this PR
Findings #3 (no concurrency token on
PluginBindingRecord), #4 (delete_agent_providerdoesn't check cross-references, no audit trail), and #5 (staleflowos-hostedguidance text, unsalted fingerprint, MCPtools/listcaching) from the same review remain open and are intentionally out of scope here (AGENTS.md rule 4: one change request per PR).Note on size
This PR is larger than the ~400-line guideline (≈1070 changed lines). The three write surfaces (two MCP tool classes, one REST controller) all need the same gate to actually close the hole — shipping them separately would leave the hole open on whichever surfaces land later. Tests and docs for the whole fix are included for the same reason.
Generated by Claude Code