Skip to content

Commit 4dddbde

Browse files
authored
Merge pull request lightspeed-core#1773 from asimurka/hitl_config
LCORE-1834: MCP Approval Configuration
2 parents 77425c2 + 333251f commit 4dddbde

8 files changed

Lines changed: 381 additions & 14 deletions

File tree

docs/config.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,40 @@ Note: this is not a real model, just an enumeration of all action names.
6262

6363

6464

65+
## ApprovalFilter
66+
67+
68+
Granular approval control for specific MCP tools.
69+
70+
Attributes:
71+
always: Tool names that always require human approval before execution.
72+
never: Tool names that never require approval (pre-approved).
73+
74+
75+
| Field | Type | Description |
76+
|-------|------|-------------|
77+
| always | array | List of tool names that always require human approval |
78+
| never | array | List of tool names that never require approval |
79+
80+
81+
## ApprovalsConfiguration
82+
83+
84+
Configuration for human-in-the-loop approvals.
85+
86+
Attributes:
87+
approval_timeout_seconds: How long approval requests remain pending
88+
before expiring.
89+
approval_retention_days: How long to retain decided approvals for audit
90+
purposes before cleanup.
91+
92+
93+
| Field | Type | Description |
94+
|-------|------|-------------|
95+
| approval_timeout_seconds | integer | Seconds before pending approval requests expire |
96+
| approval_retention_days | integer | Days to retain decided approvals before cleanup |
97+
98+
6599
## AuthenticationConfiguration
66100

67101

@@ -205,6 +239,7 @@ Global service configuration.
205239
| inference | | One LLM provider and one its model might be selected as default ones. When no provider+model pair is specified in REST API calls (query endpoints), the default provider and model are used. |
206240
| conversation_cache | | |
207241
| compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. |
242+
| approvals | | Settings for human-in-the-loop approval of MCP tool invocations |
208243
| byok_rag | array | BYOK RAG configuration. This configuration can be used to reconfigure Llama Stack through its run.yaml configuration file |
209244
| a2a_state | | Configuration for A2A protocol persistent state storage. |
210245
| quota_handlers | | Quota handlers configuration |
@@ -419,6 +454,7 @@ Useful resources:
419454
| url | string | URL of the MCP server |
420455
| authorization_headers | object | Headers to send to the MCP server. The map contains the header name and the path to a file containing the header value (secret). There are 3 special cases: 1. Usage of the kubernetes token in the header. To specify this use a string 'kubernetes' instead of the file path. 2. Usage of the client-provided token in the header. To specify this use a string 'client' instead of the file path. 3. Usage of the oauth token in the header. To specify this use a string 'oauth' instead of the file path. |
421456
| headers | array | List of HTTP header names to automatically forward from the incoming request to this MCP server. Headers listed here are extracted from the original client request and included when calling the MCP server. This is useful when infrastructure components (e.g. API gateways) inject headers that MCP servers need, such as x-rh-identity in HCC. Header matching is case-insensitive. These headers are additive with authorization_headers and MCP-HEADERS. |
457+
| require_approval | string or object | When to require human approval for MCP tool invocations. 'always' requires approval for all tools, 'never' auto-approves all tools (default), or use an ApprovalFilter for granular per-tool control. |
422458
| timeout | integer | Timeout in seconds for requests to the MCP server. If not specified, the default timeout from Llama Stack will be used. Note: This field is reserved for future use when Llama Stack adds timeout support. |
423459

424460

docs/openapi.json

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11400,7 +11400,7 @@
1140011400
"title": "AllowedToolsFilter",
1140111401
"description": "Filter configuration for restricting which MCP tools can be used.\n\n:param tool_names: (Optional) List of specific tool names that are allowed"
1140211402
},
11403-
"ApprovalFilter": {
11403+
"ApprovalFilter-Input": {
1140411404
"properties": {
1140511405
"always": {
1140611406
"anyOf": [
@@ -11435,6 +11435,52 @@
1143511435
"title": "ApprovalFilter",
1143611436
"description": "Filter configuration for MCP tool approval requirements.\n\n:param always: (Optional) List of tool names that always require approval\n:param never: (Optional) List of tool names that never require approval"
1143711437
},
11438+
"ApprovalFilter-Output": {
11439+
"properties": {
11440+
"always": {
11441+
"items": {
11442+
"type": "string"
11443+
},
11444+
"type": "array",
11445+
"title": "Always require approval",
11446+
"description": "List of tool names that always require human approval"
11447+
},
11448+
"never": {
11449+
"items": {
11450+
"type": "string"
11451+
},
11452+
"type": "array",
11453+
"title": "Never require approval",
11454+
"description": "List of tool names that never require approval"
11455+
}
11456+
},
11457+
"additionalProperties": false,
11458+
"type": "object",
11459+
"title": "ApprovalFilter",
11460+
"description": "Granular approval control for specific MCP tools.\n\nAttributes:\n always: Tool names that always require human approval before execution.\n never: Tool names that never require approval (pre-approved)."
11461+
},
11462+
"ApprovalsConfiguration": {
11463+
"properties": {
11464+
"approval_timeout_seconds": {
11465+
"type": "integer",
11466+
"exclusiveMinimum": 0.0,
11467+
"title": "Approval timeout",
11468+
"description": "Seconds before pending approval requests expire",
11469+
"default": 300
11470+
},
11471+
"approval_retention_days": {
11472+
"type": "integer",
11473+
"exclusiveMinimum": 0.0,
11474+
"title": "Retention period",
11475+
"description": "Days to retain decided approvals before cleanup",
11476+
"default": 30
11477+
}
11478+
},
11479+
"additionalProperties": false,
11480+
"type": "object",
11481+
"title": "ApprovalsConfiguration",
11482+
"description": "Configuration for human-in-the-loop approvals.\n\nAttributes:\n approval_timeout_seconds: How long approval requests remain pending\n before expiring.\n approval_retention_days: How long to retain decided approvals for audit\n purposes before cleanup."
11483+
},
1143811484
"Attachment": {
1143911485
"properties": {
1144011486
"attachment_type": {
@@ -12016,6 +12062,11 @@
1201612062
"title": "Conversation compaction configuration",
1201712063
"description": "Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default \u2014 when disabled, requests that exceed the window continue to surface as HTTP 413."
1201812064
},
12065+
"approvals": {
12066+
"$ref": "#/components/schemas/ApprovalsConfiguration",
12067+
"title": "Approvals configuration",
12068+
"description": "Settings for human-in-the-loop approval of MCP tool invocations"
12069+
},
1201912070
"byok_rag": {
1202012071
"items": {
1202112072
"$ref": "#/components/schemas/ByokRag"
@@ -14264,6 +14315,23 @@
1426414315
"title": "Propagated headers",
1426514316
"description": "List of HTTP header names to automatically forward from the incoming request to this MCP server. Headers listed here are extracted from the original client request and included when calling the MCP server. This is useful when infrastructure components (e.g. API gateways) inject headers that MCP servers need, such as x-rh-identity in HCC. Header matching is case-insensitive. These headers are additive with authorization_headers and MCP-HEADERS."
1426614317
},
14318+
"require_approval": {
14319+
"anyOf": [
14320+
{
14321+
"type": "string",
14322+
"enum": [
14323+
"always",
14324+
"never"
14325+
]
14326+
},
14327+
{
14328+
"$ref": "#/components/schemas/ApprovalFilter-Output"
14329+
}
14330+
],
14331+
"title": "Approval requirement",
14332+
"description": "When to require human approval for tool invocations. 'always' requires approval for all tools, 'never' auto-approves, or use ApprovalFilter for granular control.",
14333+
"default": "never"
14334+
},
1426714335
"timeout": {
1426814336
"anyOf": [
1426914337
{
@@ -15269,7 +15337,7 @@
1526915337
"const": "never"
1527015338
},
1527115339
{
15272-
"$ref": "#/components/schemas/ApprovalFilter"
15340+
"$ref": "#/components/schemas/ApprovalFilter-Input"
1527315341
}
1527415342
],
1527515343
"title": "Require Approval",

src/configuration.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from log import get_logger
1515
from models.config import (
1616
A2AStateConfiguration,
17+
ApprovalsConfiguration,
1718
AuthenticationConfiguration,
1819
AuthorizationConfiguration,
1920
AzureEntraIdConfiguration,
@@ -459,6 +460,20 @@ def rag(self) -> "RagConfiguration":
459460
raise LogicError("logic error: configuration is not loaded")
460461
return self._configuration.rag
461462

463+
@property
464+
def approvals_configuration(self) -> ApprovalsConfiguration:
465+
"""Return human-in-the-loop approvals configuration.
466+
467+
Returns:
468+
ApprovalsConfiguration: Settings for MCP tool approval workflow.
469+
470+
Raises:
471+
LogicError: If the configuration has not been loaded.
472+
"""
473+
if self._configuration is None:
474+
raise LogicError("logic error: configuration is not loaded")
475+
return self._configuration.approvals
476+
462477
@property
463478
def okp(self) -> "OkpConfiguration":
464479
"""Return OKP configuration."""

src/models/config.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,65 @@ def check_service_configuration(self) -> Self:
467467
return self
468468

469469

470+
class ApprovalFilter(ConfigurationBase):
471+
"""Granular approval control for specific MCP tools.
472+
473+
Attributes:
474+
always: Tool names that always require human approval before execution.
475+
never: Tool names that never require approval (pre-approved).
476+
"""
477+
478+
always: list[str] = Field(
479+
default_factory=list,
480+
title="Always require approval",
481+
description="List of tool names that always require human approval",
482+
)
483+
never: list[str] = Field(
484+
default_factory=list,
485+
title="Never require approval",
486+
description="List of tool names that never require approval",
487+
)
488+
489+
@model_validator(mode="after")
490+
def validate_no_overlap(self) -> Self:
491+
"""Ensure no tool appears in both always and never lists.
492+
493+
Raises:
494+
ValueError: If any tool name is present in both lists.
495+
496+
Returns:
497+
Self: The validated model instance.
498+
"""
499+
overlap = set(self.always) & set(self.never)
500+
if overlap:
501+
raise ValueError(
502+
f"Tools cannot be in both always and never lists: {overlap}"
503+
)
504+
return self
505+
506+
507+
class ApprovalsConfiguration(ConfigurationBase):
508+
"""Configuration for human-in-the-loop approvals.
509+
510+
Attributes:
511+
approval_timeout_seconds: How long approval requests remain pending
512+
before expiring.
513+
approval_retention_days: How long to retain decided approvals for audit
514+
purposes before cleanup.
515+
"""
516+
517+
approval_timeout_seconds: PositiveInt = Field(
518+
default=300,
519+
title="Approval timeout",
520+
description="Seconds before pending approval requests expire",
521+
)
522+
approval_retention_days: PositiveInt = Field(
523+
default=30,
524+
title="Retention period",
525+
description="Days to retain decided approvals before cleanup",
526+
)
527+
528+
470529
class ModelContextProtocolServer(ConfigurationBase):
471530
"""Model context protocol server configuration.
472531
@@ -555,6 +614,16 @@ def validate_headers(cls, value: list[str]) -> list[str]:
555614
seen.add(lower)
556615
return value
557616

617+
require_approval: Literal["always", "never"] | ApprovalFilter = Field(
618+
default="never",
619+
title="Approval requirement",
620+
description=(
621+
"When to require human approval for tool invocations. "
622+
"'always' requires approval for all tools, 'never' auto-approves, "
623+
"or use ApprovalFilter for granular control."
624+
),
625+
)
626+
558627
timeout: Optional[PositiveInt] = Field(
559628
default=None,
560629
title="Request timeout",
@@ -2051,6 +2120,12 @@ class Configuration(ConfigurationBase):
20512120
"window continue to surface as HTTP 413.",
20522121
)
20532122

2123+
approvals: ApprovalsConfiguration = Field(
2124+
default_factory=ApprovalsConfiguration,
2125+
title="Approvals configuration",
2126+
description="Settings for human-in-the-loop approval of MCP tool invocations",
2127+
)
2128+
20542129
byok_rag: list[ByokRag] = Field(
20552130
default_factory=list,
20562131
title="BYOK RAG configuration",

tests/unit/models/config/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Unit tests for models defined in config.py.
66
## [test_a2a_state_configuration.py](test_a2a_state_configuration.py)
77
Unit tests for A2AStateConfiguration.
88

9+
## [test_approvals_configuration.py](test_approvals_configuration.py)
10+
Unit tests for human-in-the-loop approvals configuration models.
11+
912
## [test_authentication_configuration.py](test_authentication_configuration.py)
1013
Unit tests for AuthenticationConfiguration model.
1114

0 commit comments

Comments
 (0)