Let a secure retriever answer the questions the editor asks it

The editor asks every retriever-backed field whether its list is open,
and whether the field is required, on endpoints suffixed onto the
field's own URL. Only the unsecured retriever had them, so opening an
MCP agent's shared session picker asked /secure-retriever/.../open and
got a NoResourceFoundException stack in the log. The editor swallowed
the failure and fell back to a closed list, which is why nothing looked
wrong.

Both questions now exist on the secure side too, defaulting the same
way, so the answer comes from the retriever rather than from a 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-10 11:35:19 +02:00
parent 90c97f9aad
commit 18f2ab2fa0
3 changed files with 56 additions and 0 deletions

View File

@ -12,6 +12,28 @@ public interface SecureDynamicFieldRetriever {
List<RetrieverItem> retrieve(String parameter, Map<String, String> params, LoginEntity user);
/**
* Whether the target field must be filled given the current parameters.
*
* <p>Answered here as well as on {@link DynamicFieldRetriever} because the editor asks the
* question of whichever retriever backs the field, and cannot know which of the two families it
* landed on.
*/
default boolean isRequired(String parameter, Map<String, String> params, LoginEntity user) {
return false;
}
/**
* Whether {@link #retrieve} returns a closed list or an incomplete one.
*
* <p>True tells the editor to accept a typed value instead of only what was listed. Closed by
* default, for the same reason as the unsecured retriever: a retriever that can enumerate its
* own values should keep the user inside them.
*/
default boolean isOpen(String parameter, Map<String, String> params, LoginEntity user) {
return false;
}
record RetrieverItemDescriptor(String label, String description, Map<String, Object> meta) {
}

View File

@ -55,4 +55,26 @@ public class SecureRetrieverController {
@AuthenticationPrincipal LoginEntity userDetails) {
return resolveRetriever(category).retrieve(parameter, params, userDetails);
}
@GetMapping("/{category}/{parameter}/required")
@Operation(summary = "Check secure retriever required",
description = "Returns true if the target field should be required given the current parameters.")
public boolean isRequired(
@PathVariable String category,
@PathVariable String parameter,
@RequestParam Map<String, String> params,
@AuthenticationPrincipal LoginEntity userDetails) {
return resolveRetriever(category).isRequired(parameter, params, userDetails);
}
@GetMapping("/{category}/{parameter}/open")
@Operation(summary = "Check secure retriever open",
description = "Returns true if the values are an incomplete list and the target field accepts a typed value.")
public boolean isOpen(
@PathVariable String category,
@PathVariable String parameter,
@RequestParam Map<String, String> params,
@AuthenticationPrincipal LoginEntity userDetails) {
return resolveRetriever(category).isOpen(parameter, params, userDetails);
}
}

View File

@ -39,6 +39,18 @@ public class SecureRetrieverControllerTest {
@Autowired
private FlowRepository flowRepository;
@Test
public void answersTheOpenAndRequiredQuestionsEveryRetrieverBackedFieldAsks() {
// The editor asks these of whichever retriever backs a field. A secure one used to have no
// endpoint to answer on, so opening an MCP agent's shared session picker logged a 404 stack.
LoginEntity user = new LoginEntity("testuser", "ignored");
assertFalse(secureRetrieverController.isOpen("ExecutionVariables", "shared",
Map.of("kind", "MCP_SESSION"), user));
assertFalse(secureRetrieverController.isRequired("ExecutionVariables", "shared",
Map.of("kind", "MCP_SESSION"), user));
}
@Test
public void flowSubFlowRetrieverReturnsOnlyValidFlowsWhenRequested() {
LoginEntity user = new LoginEntity("testuser", "ignored");