Skip to content

[Pimcore Agent] Propose import configurations through Change Control - #693

Draft
markus-moser wants to merge 74 commits into
2026.xfrom
feat/change-control-proposals
Draft

markus-moser wants to merge 74 commits into
2026.xfrom
feat/change-control-proposals

Conversation

@markus-moser

@markus-moser markus-moser commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Lets an agent propose an import configuration as a Change Control change set instead of writing it. A person approves or rejects it in the review lane.

  • MCP tools list_import_configs, get_import_config, list_config_principals and propose_import_config, with the data-importer-configuration skill. The propose tool folds the agent's document over the stored one, and refuses invented sections and fields, adapter or strategy types this installation does not register, and a change behind a switch that is off. A name nothing is stored under creates a configuration, inactive unless told otherwise. permissions can be proposed; an entry is matched by name, so the names come from list_config_principals.
  • A proposal is reviewed in this bundle's own editor, read-only, with changed fields and mapping rows marked in place and a rail that jumps to their tab. A change naming a field the configuration does not have is flagged in the rail. A new configuration gets a short per-tab summary instead of every field marked as added. Decided proposals show Change Control's outcome header.
  • Preview data can be copied or uploaded inside the review. It is scoped to the change set, so the live configuration's preview file is untouched.
  • DataImporterConfigEditor renders from a configuration it is given, so it can show a change set as well as the detail API.
  • Change Control and the MCP host are optional; without them nothing is registered.

@pimcore/change-control-bundle tracks the canary-2026.x tag for its SDK types and needs to move to a release before merge. PHPStan skips the Change Control contract classes in src/ChangeControl/ and ignores the optional symbols in src/Mcp/*, because Change Control can't be installed in CI. Overlaps with #529 in PimcoreDataImporterExtension and composer.json; whichever merges second rebases.

🤖 Generated with Claude Code

markus-moser and others added 24 commits September 9, 2026 16:15
…iven

The detail view both fetched the configuration and rendered it, so the tab tree
could only ever show what the detail endpoint returned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
transformBackendToForm() flattens general.* and the mapping list is addressed by
mappingId when stored but by index in the form, so a dotted address into the
stored document does not name the same field in the editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ritten

A configuration change an agent makes lands immediately, so nobody sees the
mapping it drops until an import runs with it.

Registered only when the Change Control bundle is installed: the handler and the
hydrator implement its interfaces, and the review surface resolves its registry by
id, so an installation without it is unaffected.

Slot addresses are document paths, not form paths - the merge takes them back as
exclude paths. mappingConfig rides one address, whole: a positional address into a
list stops being stable the moment a row is inserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… diff

A review payload carries only the changed subset of the state. Mounted on that
alone the editor rendered every untouched select empty, so the proposed leaves
now lay over the configuration the importer's own endpoint returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review offered a run button, import logs, and Add/Autofill/Delete on the
mapping list - none of which a reader can act on. It also disabled the Advanced
button, which is the one control a reader needs.

Runtime sections come off through showRuntime; the mapping affordances ask a new
read-only context, since a disabled form greys out inputs but leaves plain
buttons live.

The change rail groups by the section the editor shows the field in, folds when
the change set is large, and drops the enumeration entirely for a create - listing
every field of a new configuration says only "all of it" at length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…f it

A row now names a place in the editor and carries the reader there: the tab
switches, the Data Setup step switches, the pane scrolls and the field is marked.
Grouped tab -> section, with counts, a state filter and a field search.

The editor composes ContentLayout/Content/FormKit/Tabs itself rather than calling
data-hub's BaseDetailView. That wrapper holds no logic - it passes
defaultActiveKey, which keeps the active tab as its private state and puts it out
of reach of anything that wants to navigate to a field.

The scroll waits two frames: a Data Setup step is hidden with display:none, so a
field in an inactive step has no box to measure until the step switches and the
browser lays it out again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three MCP tools: list_import_configs, get_import_config and
propose_import_config, the last one opening a change set instead of
writing. The tool folds the agent's document over the stored one (lists
replaced whole, never merged by index), unwraps a wrapped or index-keyed
mapping list, refuses invented sections, and strips the volatile keys the
subject strips — a document the editor cannot render never reaches the
review. A skill tells the agent to read before it proposes.

The subject gets its own merge shape: the engine's structural default reads
any keyed array holding `type` or `path` as an element reference, which made
every section one leaf and an untick in the review a no-op.

The review surface diffs against the recorded base once the change set is
resolved, drops its checkboxes there, and speaks the bundle's translations.
A non-list mapping config degrades instead of taking the surface down.

PHPStan excludes the Change Control contract implementations and ignores the
optional Agent/Studio-MCP symbols; the tests that need those bundles skip
without them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rry their own marks

The rail loses its box, pills, filters and search: a caption, the tabs in
editor order, one row per changed field with its checkbox and mark, the
mapping rows underneath. It sits flush with the modal's own gutters, which
the surface was doubling.

The mapping step now marks its rows the way the form marks its fields: an
added row, a changed one with the target it had, and a dropped row left in
place, struck through. The editor shows the list in review order.

A configuration that does not exist yet reviews too: its draft is recorded
whole instead of filtered against a tree that is not there, and the editor's
steps, which read a configuration by name, find the proposed one in the
cache instead of a 404.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The propose tool now checks every adapter and strategy type — loader,
interpreter, the resolver strategies, cleanup, data targets, transformation
operators — against the service tags the factories are built from, and
refuses with the allowed list. The editor's selects cannot hold anything
else; a proposal must not either.

The review rail decides nothing any more: no checkboxes, no include state,
no "was" hints — a map only. Sections in editor order, each field named the
way the editor names it (Publish Strategy, not "type"), the mark beside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A name nothing is stored under creates one: the document is the whole
configuration, checked for what the importer cannot run without (loader,
format, target class, the resolver strategies) and for a name a file and a
YAML key accept. It starts inactive unless the proposal says otherwise —
a pipeline nobody switched on must not run because it was reviewed. The
skill tells the agent to copy the closest existing configuration rather
than write one from memory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ewed field stays visible

Cleanup strategy is only read while cleanup is on, delta check and cleanup
only with an ID field. A proposal that changes one of them while its switch
stays off would put a mark on a field the editor folds away and the import
ignores; the tool refuses it and names the switch. Where a change set does
carry such a field, the processing step keeps it on screen for the review.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…l rows read as controls

Studio's own Content spinner while the change set loads, no count reported
before it has been read, and rail rows bordered and filled so they read as
something to press rather than as text on the page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…them is a listing

Each section headline — at body size — carries one arrow that opens that
tab and step. The fields beneath are no longer controls: a plain listing of
name and mark, one hairline apart. The per-field scroll-to-anchor goes with
it; the section is where the reader lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@markus-moser markus-moser changed the title [Change Control] An import configuration can be proposed instead of written [Change Control] An agent can propose an import configuration, and a steward reviews it in the editor Sep 11, 2026
markus-moser and others added 3 commits September 11, 2026 14:38
…s read as a list

The arrow is an ornament on a headline that is the button; the fields under
it are a bulleted, hairline-separated list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ays it is new

The create marks its proposal isNew, so the card and the review title say
"new" instead of "changed". The rail no longer replaces the map with a
summary for a create: the same sections and fields, every one added, with
the subject's own name, type and path left out — those are identity, not a
change anyone weighs. The list drops its bullets and indent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
markus-moser and others added 28 commits September 14, 2026 15:48
The card was written against a 14px base and Studio's is 12, so sizes derived
as fontSizeSM minus one or two rendered at 9px and 8px — the section labels and
the notes under each step were below anything the scale contains. They take
fontSizeSM now, the name takes fontSizeLG, and the state pill takes fontSize so
it reads as one of the tags beside it. Letter-spacing stays a literal: the
scale has no token for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things a review round turned up. A failed request reached the user as
"Something went wrong": Studio's ApiError reads `message`, `error` or
`errorKey`, and a Symfony HTTP exception serialises as problem+json with its
sentence in `detail`, so a missing source asset never said so. Every place the
importer surfaces a request failure now passes the sentence through, wherever
it landed in the body. The rail takes the modal's own 24px gutter, so its
content lines up with the title above it instead of sitting 4px inside. And two
changed fields in one section no longer stack their marks edge to edge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adopted from the Change Control bundle, which settled this on 2026-09-11: one
line carrying the state with its icon, when it was resolved, and the read-only
notice folded into a hint rather than spelt out underneath. The header also
leaves the rail and runs the width of the modal body, which is where the title
above it sits. Its translation key was already on the branch, swept into an
earlier commit of mine, so this is the half that was missing.

The horizontal inset on that row is mine: the modal body reaches the modal's
edges, so the row needs the gutter the title keeps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rail for a changed configuration and the brief for a new one were this
bundle's own card, copied once already into Webhooks. Both now map onto the
SDK's summary - sections of rows for the changes, one stop per section for the
brief, and its foot for what is waiting in the editor - and the card, its styles
and the status tag are deleted here. The mapping step's mark is the SDK's too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The surface registered through a locally declared registry interface with a
register() the lane has not had since it moved onto Studio's dynamic types, so
the guard was silently false and no importer review surface was ever registered.
Typing the seam against @pimcore/change-control-bundle is what catches that.

mode followed: the lane resolves the stance and sends compareAgainst, never mode,
so history was unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It moved there with #96, so the importer no longer reaches into the Data Hub
for a review component. StatusTag folded into the lane's own ChangeTag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three refusals the propose tool returned inline now throw one refusal type, which
takes the branching out of execute; unknownValues judges one leaf at a time; the
vocabulary takes its tagged iterators as one map keyed by the family it prints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FLATTENED is only ever asked whether it holds a segment. A dataTarget language
that is not a string cannot key a mapping row, so it keys as absent rather than
stringifying to [object Object].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s not have

An invented leaf was recorded verbatim: only top-level sections and typed
values were checked, so processingConfig.doDeltaCheckCheck reached a change
set, reviewed as a change and applied as nothing.

propose_import_config now refuses a leaf the document does not name and the
installation does not already store; `settings` subtrees and the mapping list
stay open, their keys belonging to the type they configure. The review surface
marks a change it cannot bind to a field, which also catches the residue in
configurations already written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The warning rode the row's hint, which the summary renders only as a title
attribute. It is the row's note now, which the summary draws under the label.

The rail also reaches into the modal body's gutter: the summary's spine
markers hang to the left of their text, and the overflow rule was clipping
them away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Giving the scroller the gutter was half of it: the rail itself had no left
padding, so what the scroller reached into fell outside the rail and the
markers were still clipped. Both boxes borrow it from the modal body now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The summary draws its own card now, so the rail's divider and the gutter it
borrowed for the hanging spine both go. The nine brief* rules go with them:
they drew the card this replaces and nothing has referenced them since the
summary moved into the SDK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new-configuration rail showed four pipeline stops while the change rail
showed the editor's sections, so the two read as different things. It now
walks the same sections in the same order and opens the same steps.

A section that is all new has nothing to mark field by field, so it says what
it is set to and how much of it the line did not get to — "Asset · JSON /
/Import Test/car-export.json · 2 more settings". The tail counts only leaves
the line did not already name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tings"

General counted one setting and said so twice over: the name, description and
on/off state it holds are the identity header directly above the sections, and
the only thing left is the group. It now appears when a group is set and is the
header otherwise.

Counts read as counts: a key for one and a key for the rest, the way the
summary already picks between "1 change" and "%s changes".

The aggregate foot goes — each section carries its own count now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three mapping columns want room; the shell should not be the one knowing that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
permissions was always an accepted section, but nothing said so: the propose
tool listed seven sections and the skill never mentioned it, so an agent asked
for permissions inferred a prohibition and invented a security rationale for it.

Both now describe the section and its shape. list_config_principals gives the
names to put in it — an entry is matched by name against the logged-in user's
own, so a name this installation does not have grants nothing while reading to
a steward as though it does. Admins are left out; they pass every check anyway.

The review guard learns the section too, or it would mark a permissions change
as naming a field the configuration does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HistoryHead was this bundle's copy of the change-control review header — its own
comment said so. The header now carries that design and ships from the SDK, so
the copy goes, along with its four styles and the six strings per locale that
only it used. The band draws its own gutter and rule, so the surface wraps it in
the gap="small" Flex its bottom margin is cut to cancel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The registration declares a layout; the published package only gained that
type this morning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… tracks canary-2026.x

The modal body already gives the surface its gutter; the outcome band's own put
its tag one gutter in from the title and the rail. The surface cancels it.

The SDK dependency goes back to the canary-2026.x tag, which now carries the
surface layout the registration declares, so the exact pin is no longer needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The change rail puts its unbound-field warning in a row's note; the summary in
the previously locked canary never drew it in the changes variant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@markus-moser markus-moser changed the title [Change Control] An agent can propose an import configuration, and a steward reviews it in the editor [Pimcore Agent] Propose import configurations through Change Control Sep 24, 2026
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