Problem
The React view layer has a headless control surface (com.top_logic.layout.react.scripting), built under #29108. It serves three consumers off one mechanism: the script recorder, deterministic replay, and an agent driving a session on a user's behalf.
The substrate works and is verified live against the demo:
- observe / resolve / act on ScriptingSession, dispatching through the very same ReactControl.executeCommand the browser uses.
- Stable semantic addressing (ScriptingTreeProjector): role[name] paths plus navigation slots, with business-key (ModelName) identity for data nodes; address drift hard-fails instead of silently substituting.
- Recorder: browser commands captured as typed ReactCommand configuration items, replay-stable arguments (selectByKey), assertion steps, a recorder side window with a step debugger.
- Affordance-first observe?mode=actions projection and JSON argument schemas derived from the command's ConfigurationItem descriptor.
What was missing is everything that turns a working prototype into a feature: the authorization and session model for non-browser callers, the read-concurrency model, and productionization.
Done
Command handlers refuse what the user interface does not offer
Acting goes through the same dispatch as the browser, so a command whose executability is re-evaluated at execution time was already safe — but not every path was. A scan of all 84 @ReactCommandHandler methods found and closed four classes of gap: field writes that ignored editability, form lifecycle commands that bypassed their own command models, dispatcher commands that invoked an action without re-checking it, and a button whose CommandModel granted execution unconditionally. The field check is structural: one final handler funnels every client value through a single guarded entry, and subclasses override the apply/commit hooks below it.
A form can now be configured with the permission to edit its object at all (<edit-executability> taking the usual ViewExecutabilityRule`s, e.g. a `SecurityRule); previously there was no configuration point for the permission that opens write access.
An account invites an agent into its session
An account issues an invitation — the application's publicly reachable address together with a token, in one line, so an agent is invited with a single copy. The token is bound to the session it was issued from and dies with it; only its hash is stored and the plain value is readable exactly once. Scope is observe or observe and act; an observing token is refused on every writing request. Owners withdraw their own tokens, administrators everyone's, withdrawing deletes them, and a daily task deletes the expired ones.
A module of its own
The agent-specific part left the core React module: com.top_logic.layout.view.agent (tl-layout-view-agent) holds the AgentServlet with its /agent-api/* mapping, the admission (AgentAuthenticator), and the access tokens with their model, views, cleanup task and admin section. The interface itself stays in the core module, where the script recorder consumes it, and is named after the legacy application scripting. An application that does not want agent access does not deploy the module — which also settles the per-environment enablement question.
Remaining work
An agent with a session of its own
The second usage mode: an agent working on behalf of an account independently of an interactive session. Authentication can reuse the shape already in place, but the React control tree is created by ViewServlet on page load and fills in during rendering, so a session created without a browser projects an empty application. That headless window bootstrap is the substantial part, and it is independent of every authorization decision.
Also open: whether issuing a token should itself be permission-gated (so an application can forbid agent access outright), and whether the view layer should enforce ModelAccessRights — its checks are opt-in in TL-Script and deny by default when nothing is configured, so switching them on is a policy decision, not a bug fix.
Read concurrency and stability (D6)
- Snapshot-under-lock, project-off-lock: observe holds the session-wide request lock across the whole projection, including label resolution, so it can stall real user clicks.
- The SSE heartbeat's synthesizeModelEvents mutates the control tree without the request lock.
- A real quiescence signal beyond the synchronous case.
- Investigate the per-window queue recreation that shows up as "controls don't react".
Recorder completion
- Export / clear from the recorder UI; an on-disk script format.
- Cross-session round-trip test of the addressing scheme.
- Migration story and coexistence with the legacy ScriptingRecorder; the parity backlog from the gap analysis (typed assertion library, variables/parameterization, script control flow).
External agent access (MCP)
- A thin MCP server mapping observe / list_actions / act / wait_for_settled onto the endpoint.
- End-to-end: an external agent drives a live TopLogic session.
Productionization
- Servlet-level integration test (boots the container, not only unit tests).
- Performance on large views: projection cost, lock contention, bounded observation size (paging / subtree addressing).
- Developer documentation: how to make a control well-addressable and well-described.
- Decide the remaining open design questions D3 (one artifact or two) and D4 (raw tree vs. curated projection).
Reference
Plan and progress log: com.top_logic.layout.react/doc/headless-interface.md.