Repository navigation
reactor.ui 1.1.0: one wire between a Python host and the page (RU-0) - #27
Merged
Merged
Conversation
Python `reactor.ui`: the envelope {v:1, op, scope, id, body} with eight ops
(show|patch|append|remove|state|ask|window|meta) as pydantic models; Display
(typed fields as `data`, mandatory text(), model_view()); @display_type with
MIME-like types and versions; audiences (`both`, `page`, secrets for neither);
ActionRegistry (HMAC tokens bound to session + display + action, unknown
action 422, bad token / veto 403, idempotency ids); UiChannel and
MemoryUiChannel; an optional AG-UI encoder; the JSON Schema, committed as
schema/reactor-ui.v1.schema.json and written by `reactor ui schema`. Hooks
provide_display_types and authorize_ui_action (any False or a raise vetoes).
TypeScript `@datalayer/reactor/ui`: wire types held to the schema by a drift
test, the Renderers and ToolBindings contribution points, resolveRenderer
(rank, then registration order, version range), resolveToolResult (embedded
_meta["reactor.ui/display"] first, then a binding), and a pure reducer with
RFC 6902. `@datalayer/reactor/ui/react`: ReactorDisplay (lazy renderer, text
fallback) and useUiChannel.
Docs: docs/docs/ui-channel. Every package bumped to 1.1.0.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved security, wire-validation, AG-UI patching, compilation, and idempotency issues block approval.
9 open findings
Use length-prefixed components for signing input · New Prevent prototype pollution during JSON Pointer traversal · New Handle undefined JSON.stringify results · New Validate operation-specific fields in UiOp type guard · New Make provider registry updates atomic · New Do not evict unresolved run-once entries · New Rebase JSON Patch move and copy source paths · New Require operation-specific fields in JSON Patch schema · New Use structural equality for JSON Patch test operations · New
What changed in this PR
Introduces Reactor UI’s typed Python-to-page data plane, renderer APIs, React integration, schema tooling, documentation, and release updates.
Changes:
- Adds Python wire, display, action, channel, AG-UI, schema, and plugin-hook APIs.
- Adds TypeScript renderers, reducers, React components/hooks, and tests.
- Publishes the new UI exports and bumps release packages to 1.1.0.
| File | Description |
|---|---|
vitest.config.ts |
Adds UI package aliases. |
tests/test_ui_schema.py |
Tests schema drift and CLI output. |
tests/test_ui_display.py |
Tests displays, audiences, channels, and AG-UI. |
tests/test_ui_actions.py |
Tests action authorization and idempotency. |
src/ui/wire.ts |
Defines TypeScript wire contracts and guards. |
src/ui/renderers.ts |
Adds renderers and tool-result resolution. |
src/ui/reducer.ts |
Implements UI state and JSON Patch reduction. |
src/ui/index.ts |
Exports framework-independent UI APIs. |
src/ui/__tests__/wire.test.ts |
Tests schema drift and guards. |
src/ui/__tests__/renderers.test.ts |
Tests renderer and binding resolution. |
src/ui/__tests__/reducer.test.ts |
Tests reducer and patch behavior. |
src/react/ui/useUiChannel.ts |
Adds the React channel hook. |
src/react/ui/ReactorDisplay.tsx |
Adds pluggable display rendering. |
src/react/ui/index.ts |
Exports React UI APIs. |
src/react/ui/__tests__/ReactorDisplay.test.tsx |
Tests rendering and channel integration. |
scripts/generate-ui-types.mjs |
Adds optional schema-to-TypeScript generation. |
schema/reactor-ui.v1.schema.json |
Commits the v1 wire schema. |
reactor/ui/wire.py |
Defines canonical Python wire models. |
reactor/ui/schema.py |
Generates the wire schema. |
reactor/ui/display.py |
Adds display types and audience filtering. |
reactor/ui/channel.py |
Adds channels and operation helpers. |
reactor/ui/agui.py |
Encodes operations as AG-UI events. |
reactor/ui/actions.py |
Adds signed, authorized UI actions. |
reactor/ui/__init__.py |
Exposes the Python UI API. |
reactor/reactor.py |
Integrates display and authorization hooks. |
reactor/hooks.py |
Declares new UI plugin hooks. |
reactor/cli.py |
Adds the UI schema command. |
pyproject.toml |
Adds the optional AG-UI dependency. |
plugins/shell/package.json |
Bumps shell to 1.1.0. |
plugins/manager/package.json |
Bumps manager to 1.1.0. |
plugins/graph/package.json |
Bumps graph to 1.1.0. |
plugins/commands/package.json |
Bumps commands to 1.1.0. |
package.json |
Adds UI exports, schema files, and version bump. |
docs/docs/ui-channel/index.md |
Documents the UI channel. |
docs/docs/ui-channel/_category_.json |
Adds the UI documentation category. |
docs/docs/mcp-server/_category_.json |
Reorders MCP documentation. |
docs/docs/examples/_category_.json |
Reorders examples documentation. |
docs/docs/core-plugins/_category_.json |
Reorders plugin documentation. |
apps/mcp-server/reactor_mcp_server/__init__.py |
Updates fallback version. |
apps/mcp-server/pyproject.toml |
Updates MCP package and dependency versions. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def mint(self, session: str, display_id: str, action: str) -> str: | ||
| """The token that lets this session press this action on this display.""" | ||
| self.get(action) | ||
| message = "\0".join(("reactor.ui/v1", session, display_id, action)).encode() |
Comment on lines
+64
to
+65
| } else if (typeof node === 'object' && node !== null && token in node) { | ||
| node = (node as Record<string, unknown>)[token]; |
| return result; | ||
| } | ||
| try { | ||
| return JSON.stringify(result); |
Comment on lines
+163
to
+169
| export function isUiOp(value: unknown): value is UiOp { | ||
| return ( | ||
| isRecord(value) && | ||
| value.v === UI_WIRE_VERSION && | ||
| typeof value.scope === 'string' && | ||
| UI_OP_NAMES.includes(value.op as UiOpName) | ||
| ); |
Comment on lines
+1109
to
+1112
| try: | ||
| self._run_plugin_call(plugin_name, lambda: provider(registry)) | ||
| except Exception as error: # noqa: BLE001 | ||
| logger.warning("Plugin %s failed to provide display types: %s", plugin_name, error) |
Comment on lines
+215
to
+217
| self._done[key] = future | ||
| while len(self._done) > self._remember: | ||
| self._done.popitem(last=False) |
Comment on lines
+54
to
+57
| patch=[ | ||
| {**operation.model_dump(mode="json", by_alias=True, exclude_unset=True), "path": "/display" + operation.path} | ||
| for operation in op.body | ||
| ], |
Comment on lines
+77
to
+80
| op: Literal["add", "remove", "replace", "move", "copy", "test"] | ||
| path: str | ||
| value: Any = None | ||
| from_: str | None = Field(default=None, alias="from") |
Comment on lines
+157
to
+159
| function equal(a: unknown, b: unknown): boolean { | ||
| return JSON.stringify(a) === JSON.stringify(b); | ||
| } |
- actions: the token's HMAC message length-prefixes each part (8-byte big-endian length + bytes), so one token binds exactly one tuple. - actions: a running press is never forgotten; only settled ones make room, and a press arriving while every remembered press runs is refused with UiActionBusy (429). - display types: each provider declares into its own registry, merged only once it succeeded (all-or-nothing, conflicts included). - agui: move/copy `from` is rebased under /display with `path`. - wire: a patch operation is an op-discriminated union (PatchAdd, PatchRemove, PatchReplace, PatchMove, PatchCopy, PatchTest) with the members RFC 6902 requires; schema regenerated, TS types and drift fields follow. - reducer: pointers walk own properties only and refuse __proto__, constructor and prototype; `test` compares structurally, not by key order. - wire.ts: isUiOp checks each op's id and body as the Python models do. - renderers: textOf falls back to String() when JSON.stringify has no answer.
… (CI installed .[dev] without the agui extra)
…s the operations, 0.1 keeps dicts)
…tocol 0.1.19 and 1.0.0)
…e (PR #27) docs/docs/ui-channel: an overview (one wire, displays with a text and what a model sees, renderers keyed by type, actions with tokens) and a page each for displays and audiences, renderers and tool bindings, the channel ops and the reducer (JSON Patch rules, pointer safety), actions (tokens, the veto hook, idempotency, 403/422/429), AG-UI, the schema and types, and security. Linked from the docs index, both getting-started pages and the examples index. examples/ui-channel: an approval card from a FastAPI host to a React page and back -- a @display_type with page-only fields and a secret, a plugin that declares the type and vetoes a requester deciding their own request, the ops a decision emits, and a page whose plugin contributes a lazy renderer and a tool binding, keeps the ops with useUiChannel and presses with the offered token. A recorded session (fixtures/demo-ops.json) is checked by the Python test and replayed by the vitest tests, which the root vitest config now includes.
…TOR_UI RU-2) — ACTIVITY_SNAPSHOT/DELTA and CUSTOM reactor.ui back into ops, scope kept per display, anything of the reactor's not its shape refused; one committed example stream holds encoder and decoder together
…b (a UiChannel: ops numbered and kept per scope, handed to every connection on it) and UiSocket (the ASGI endpoint a host mounts): hello first with the credential (never in the URL), welcome, ops replayed after since then live, action and state frames answered by result frames with the registry's status, pings both ways, a bounded queue per connection (4429), every frame checked against the schema (4400), 4401/4403/4408/4410; the frames in reactor.ui.wire and the committed schema. Page: @datalayer/reactor/ui/ws UiSocket (ordering, ops applied once, gap and drop resume from the last op, presses sent again with their idempotency id, refusal stops, heartbeats) and useUiSocket in ui/react. One committed exchange (schema/reactor-ui.v1.ws-examples.json) written by tests/test_ui_ws.py and replayed by src/ui/__tests__/ws.test.ts; two tabs on both tiers. The approval card example moves from polling and an action POST to the socket. Docs ui-channel/websocket
…sh hello, up to the welcome's seq) — drawn again, never acted on as news; useUiSocket raises onWindow for new window ops only (REACTOR_UI RU-W)
…both tiers (REACTOR_UI P5, RU-4): what a display shown by ask offers, the host checking the value against what it asked; docs ui-channel/actions, the page pressing up the socket
….1 and 1.0 order a JSON patch's keys differently (CI on 1.0.0)
…RU-4) and AguiUiDecoder (RU-2) on top of 1.1.0's ui channel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Stacked on #26 (
feat/apps-mcp-server, which carries the 1.0.5 release): branched from it rather thanmainso 1.1.0 follows 1.0.5. When #26 merges this retargets tomain.RU-0 of the Reactor UI plan: a typed data plane between a Python host and the page.
Python:
reactor.uiwire.py: the envelope{v:1, op, scope, id, body}with eight ops (show|patch|append|remove|state|ask|window|meta), a discriminated union in pydantic. AlsoDisplayWire,ActionOffer,UiAction,FileRef, and theDisplaybase class: its typed fields go on the wire asdata, andtext()andmodel_view()are required.display.py:@display_type(type, version=)with MIME-like types, andDisplayRegistry/DISPLAY_TYPES. Audiences:audience("both"|"page");SecretStris in neither view.model_view()/page_view()also work on action payloads.actions.py:ActionRegistry(secret, platform=)withregister/offer/authorize/dispatch.UnknownUiAction(422). Bad token: 403. Veto: 403. Invalid payload: 422.channel.py: theUiChannelprotocol,MemoryUiChannel, and a helper for each op.agui.py: an optional encoder.showbecomesACTIVITY_SNAPSHOT;patchandappendbecomeACTIVITY_DELTA;statebecomesSTATE_DELTA; anything else isCUSTOM reactor.ui. New extra:agui.schema.pyandreactor ui schema --out ...; the output is committed asschema/reactor-ui.v1.schema.json.provide_display_types(registry)andauthorize_ui_action(principal, session, action). AnyFalsevetoes, and so does a hook that raises.PluginPlatform.display_types()andPluginPlatform.authorize_ui_action()call them.TypeScript:
@datalayer/reactor/uiand@datalayer/reactor/ui/reactUI_WIRE_FIELDSis checked against the schema by a drift test.scripts/generate-ui-types.mjsuses json-schema-to-typescript when it is installed; it is not a dependency.Renderers(reactor.ui.renderer:{type, versions, rank, component|load}) andToolBindings(reactor.ui.toolBinding:{tool: string|RegExp, type}).resolveRenderer: the highest rank wins, then registration order; only renderers whose version range admits the display are considered.resolveToolResult: an embedded_meta["reactor.ui/display"]comes first, then a binding, otherwiseundefined.applyUiOp: a pure reducer, RFC 6902 forpatchandstate, failing fast.<ReactorDisplay display act context fallback loading/>draws the display with a lazy renderer and falls back to its text.useUiChannel({scope, onWindow})keeps the reducer's model in a component../uiand./ui/react.Docs
docs/docs/ui-channel/.Tests
pytest tests: 154 passed, 23 of them new: display, model_view and audiences; actions (token, veto, unknown → 422 at a route, idempotency); schema drift and the CLI.vitest run src/ui src/react/ui: 21 passed, covering the wire drift, renderer rank, order, version range and disposal, tool resolution order, the reducer, ReactorDisplay and useUiChannel.Every package is bumped to 1.1.0 with
dev/bump_version.py minor. Do not tag yet.