Issue #3583059: Pass config entities across handle boundaries as machine names

What changed

An invoker that asks for handles now sends and receives machine names for config entities:

// Input schema for entity:user_role, before and after.
{"$comment": "No schema is defined for property of type ConfigEntityAdapter..."}
{"type": "string", "description": "Role: A user role. (pass the machine name of the user_role)"}

// Published outputs, before and after.
{"role": {}, "roles": [{}]}
{"role": "editor", "roles": ["editor", "reviewer"]}

Why

#3583028 (closed) let entity: inputs take config entities, but the handle subscriber only knew content entities. Over MCP or the AI connector, a config entity input had no schema, a machine name failed validation, and config entity outputs came back as {}. Closes #3583059.

How

  • EntityHandleTransformer::isConfigEntityDefinition() matches entity:TYPE when TYPE is a config entity type. The bare entity type is left alone, since loading by ID needs the type.
  • transformInputDefinition() and transformOutputDefinition() swap those definitions for strings and name the entity type in the description.
  • resolveConfigInput() loads the machine name through the entity type's storage and throws MachineNameNotFoundException when it does not exist. It implements HandleExceptionInterface, so the runner still reports an input failure on the input's path, but it is not a HandleNotFoundException: the runner adds handle ownership and expiry advice to that one (#3582977), which is wrong for a machine name.
  • transformConfigOutput() publishes the ID with a hint naming the entity type, without the label, so no view label access check is needed. EntityHandleTransformSubscriber does the same for each config entity in a list output: "Output roles contains 2 user_role machine name(s)."
  • A list of config entities needs nothing extra. A multiple: TRUE input is converted to a ListInputDefinition when it is declared (#3583049 (closed)), so the subscriber only ever sees the single item definition. Coercion decodes or wraps the list first, and each item resolves as a machine name.
  • docs/developers/entity-handles.md has a "Config entities" section.

Invokers without EntitiesAsHandles see no change.

Testing

  • EntityHandleTransformSubscriberTest::testConfigEntitiesCrossAsMachineNames runs config_entity_tool with a machine name and a list of machine names in, and a single role and a list of roles out.
  • The list-shape tests mirror the content ones: testBareMachineNameForListInputIsWrappedThenResolved, testJsonEncodedMachineNameListIsDecodedThenResolved, and testEmptyStringForOptionalListInputIsNoValue.
  • testUnknownMachineNameIsRejected covers the MachineNameNotFoundException, and ToolRunnerTest::testUnknownMachineNameNamesTheInputWithoutHandleAdvice checks the violation the model sees has no handle advice. It fails when the exception is swapped back.
  • EntityHandleTransformerTest covers the transformer methods and the schema swap.
  • AdvertisedOutputSchemaConformanceTest now runs config_entity_tool with a saved role instead of skipping it, so the formatted role and roles values are validated against their string schemas. Breaking transformConfigOutput() locally fails it.
  • AdvertisedSchemaAcceptanceTest gets role overrides for the new tool.

🤖 Generated with Claude Code

Edited by Matt Glaman

Merge request reports

Loading
Loading