Skip to content

Finding #1: require agent.manage capability for agent-provider writes - #149

Merged
ObaidulKabir merged 1 commit into
mainfrom
fix/agent-provider-governance-gate
Nov 6, 2026
Merged

ObaidulKabir merged 1 commit into
mainfrom
fix/agent-provider-governance-gate

Conversation

@ObaidulKabir

Copy link
Copy Markdown
Owner

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 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 on the same write surface are closed in the same change, since they're part of the same exposure:

  1. Endpoint-swap key exfiltration. Changing only endpoint kept the stored BYO API key unchanged, silently sending it (as a Bearer token) to whatever new host was saved.
  2. Unrestricted http://. Endpoint URLs were only checked for shape — any scheme, including a bare http://, was accepted in every environment, exposing the key on the network path.

What

  • New capability agent.manage (AgentProviderKinds.ManageCapability), granted to the reserved Admin role's default capabilities only (not ApiKeyCapabilities), plus an opt-in API-key scope mapping agent:manage for tenants who want to grant it to a narrower role later via grant_role_capability. TenantSecurityProvisioningService backfills new AdminCapabilities onto 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 to Admin, so nothing breaks; only a narrowly-scoped non-full-access API key is newly blocked, which is the point.
  • Capability gate on all four write surfaces: upsert_agent_provider, delete_agent_provider, backfill_agent_provider_key_metadata (AgentContextMcpTools), register_plugin_binding's agent path (PluginBindingMcpTools), and REST PUT /api/plugin-bindings's agent branch (PluginBindingsController). A denied caller gets MCP-AUTHZ-001 (MCP) or 403 Forbidden (REST).
    • New shared TenantCapabilityGate.HasCapabilityAsync helper, extracted from the existing TenantTeamMcpTools/TenantIamMcpTools pattern, so all three call sites share one check instead of three copies.
    • AgentContextMcpTools/PluginBindingMcpTools take ICurrentUser/ICapabilityService as optional trailing constructor params (matching this project's existing optional-param convention for toolHost/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 via ToolRegistration's scope.ServiceProvider.GetRequiredService<T>(). PluginBindingsController takes ICapabilityService/IWebHostEnvironment/IConfiguration as required params since ASP.NET Core always supplies them.
  • Endpoint-host-change clears the stored key. AgentProviderConfiguration.Merge() now clears ApiKey/ApiKeyFingerprint/ApiKeyUpdatedAtUtc when the caller supplies a new Endpoint but not a new ApiKey, and the new endpoint's host differs from the previous one (Uri.Authority comparison, case-insensitive). A path- or scheme-only edit to the same host leaves the key untouched; re-supply apiKey in the same call when repointing to a new host.
  • https:// required outside Development. New TenantIdentityRules.AllowInsecureAgentEndpoints(environmentName, configuredValue) (mirrors the existing DemoKeysAllowed idiom exactly) — explicit override via FlowOS:Agents:AllowInsecureEndpoints, else true only in Development. Enforced identically (duplicated per-class, since each resolves environment from a different source) in AgentContextMcpTools, PluginBindingMcpTools, and PluginBindingsController. register_plugin_binding's agent path also gained the endpoint shape+scheme validation it was previously missing entirely (unlike upsert_agent_provider, which already validated shape).

Tests

  • New 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.
  • New capability-gate and https-enforcement tests in AgentContextMcpToolsTests (6 cases: deny/allow on upsert, deny-before-delete, deny on backfill, http rejected in Production, http allowed in Development) and PluginBindingMcpToolsTests (4 cases: deny/allow on register_plugin_binding's agent path, non-URL endpoint rejected, http rejected outside Development).
  • New 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.
  • Confirmed (grep across tests/) no pre-existing test in this codebase supplies a plain http:// agent-provider endpoint, so the new default-to-strict scheme check breaks nothing already passing.

Docs

  • docs/08-security-roles-and-policies.md: agent.manage added to the "Recommended capability codes" list.
  • docs/07-ai-agents-and-insights.md and FlowOsMcpGuidance.cs: describe the capability requirement, the https://-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_provider doesn't check cross-references, no audit trail), and #5 (stale flowos-hosted guidance text, unsalted fingerprint, MCP tools/list caching) 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

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
@ObaidulKabir
ObaidulKabir merged commit f88d64e into main Nov 6, 2026
3 checks passed
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