React Flow and ELK render the graph; Rust owns authoring, admission, storage, and replay. See UI setup for usage and run observation for live monitoring and replay.
From the repository root:
npm --prefix ui ci --ignore-scripts
npm --prefix ui run build
cargo run -p zeroshot --features ui -- uiOpen http://127.0.0.1:4173/ui/; use --listen 127.0.0.1:PORT for another loopback port.
Run this loopback server only on a trusted single-user host. It is not an isolation boundary from
other OS users or local processes.
The optional Cargo ui feature embeds ui/dist. Release binaries and target images enable it;
Node is needed only at build time. Rebuild the frontend before Rust after UI edits.
Local profiles share the CLI store (ZEROSHOT_CONFIG_DIR); ledgers use ZEROSHOT_STATE_DIR.
Pass --target NAME to keep those profiles local while reading history from a configured direct or
hosted target. Hosted credentials stay in the local UI server and never enter the browser. Use
temporary absolute directories for isolated tests. Ctrl-C stops the UI while detached runs continue.
Direct target serve mounts /ui/ on its existing listener, stores profiles/history under
--storage, and requires the browser's exact --public-origin. Private/hosted targets exclude
this mount. See the target image guide for persistence and restart behavior.
The bootstrap harnesses catalog supplies each host-enabled adapter's id, label, and
providers. The editor uses this catalog for runtime choices; runtimeSchema supplies structural
fields such as run size and effort. For older hosts that omit the catalog, choices come from their
legacy runtime schema; an explicit empty catalog offers no adapters. Local catalogs include compiled
contributed adapters; target and Platform catalogs use their
host's enabled registry. Existing unavailable selections remain visible and stay in the profile until
the author changes them.
Hosting shells may supply a machines array in the existing bootstrap. Each entry has size,
label, vcpus, memoryMiB, diskGiB, laneUnits, and optional
gpu: {name, memoryMiB} or null. The shared size picker displays the host label and lane count. Starting a size with Boost uses its full laneUnits in Boosts for that run; ordinary
runs do not spend Boosts merely because a size is selected. This is presentation metadata: the host
owns admission and billing. An explicit empty list offers no machines. When the array is absent,
the native RunSize schema supplies size identities without resource or pricing claims. Unavailable
saved selections remain visible until the author changes them. Resource descriptors never enter
the saved runtime plan.
Profiles contain graphs and agent runtime configuration. Environments belong to run submission, not profile authoring. Plain local UI has no environment catalog or management page.
Local profile lists return readable metadata in profiles and malformed entries in optional problems,
with each entry's name, default status and diagnostic. The sidebar shows these problems alongside
usable profiles. Graph/runtime schemas are checked when a profile is opened; unrelated saves and
metadata operations preserve incompatible entries. Target v1 profile responses retain their original
profiles-only contract; diagnostics are local CLI/UI data. Invalid shared store JSON remains a store error.
For frontend development, keep the native server running:
npm --prefix ui run devOpen http://127.0.0.1:5173/ui/. Set ZEROSHOT_UI_TARGET on the Vite command to use a different
backend origin. The development proxy adapts its known loopback origin; production serves embedded assets.
| Owner | Responsibility |
|---|---|
App.tsx, workspace-services.ts |
Shared editor and injected profile, authoring, and history services |
WorkflowCanvas, editor |
Shared graph presentation, draft/undo, and read-only observation |
profile_ui.rs, profile_ui/{data,outcomes}.rs |
Native catalog, draft transformations, admission, and conditional profile saves |
profile_ui/server.rs |
Embedded assets, browser origin checks, and server/observer lifetime |
native_v2_observability::history, run-history-source.ts |
Admitted definitions, native control projection and resumable observation |
/ui/api/data and /ui/api/authoring return drafts. Save validates through native admission and
checks both the content revision and persisted workspace identity. The browser acknowledges only
the saved document generation, preserving newer edits. Layout/theme preferences stay out of profiles.
Run definitions come from admission snapshots. History preserves native event order and opaque cursors; Rust reconstructs structural visits through its canonical reducer. The browser projects only the selected prefix. Unavailable/incomplete history remains explicit, with recorded worker history retained. Observation never starts or recovers a controller.
The history endpoints are GET /ui/api/runs, GET /ui/api/runs/{id},
GET /ui/api/runs/{id}/history?after=CURSOR, and GET /ui/api/runs/{id}/events?after=CURSOR.
These are the local browser BFF routes. A selected target is read server-side through its discovered
zeroshot.run-history/v1 list/detail/page templates; target URLs and authorization are not browser
contracts.
Omit after to begin. SSE resumes from Last-Event-ID and closes after the terminal cursor drains.
Build ui/dist from Platform's exact pinned Zeroshot source revision and mount it on the
Platform origin independently of run containers. The shell-less entry is embed.html;
index.html retains the standalone toolbar. Both entries use the same editor and viewer.
Platform must permit its same-origin frame and the workspace's bundled assets/ELK worker in CSP.
The API-gateway can call zeroshot_engine::workspace::{catalog, author, data, validate_profile}
with Cargo's workspace feature; this does not embed UI assets. Authenticated private targets
export native definition/history pages separately from the standalone UI. See the
approved implementation plan.
workspace-bridge.ts defines the wire types. All messages use postMessage with the exact
same origin and expected parent/iframe window. The iframe first sends
{version: 1, type: "ready", requestId}. Reply with init using that request ID:
{
"version": 1,
"type": "init",
"requestId": "ready-request-id",
"workspaceId": "opaque-user-org-scope-workspace-id",
"authority": { "userId": "user-id", "organizationId": "org-id", "scope": "user" },
"apiBase": "/_bff/orgs/org-id/workspace/user/",
"theme": "light",
"csrf": { "cookieName": "__Host-zsc-csrf", "headerName": "X-Zeroshot-CSRF" }
}The authenticated bootstrap must return version: 1 and
workspace: {kind: "platform", id: workspaceId}. Bind this ID to user, organization and profile
scope. Recreating the iframe binds a different authority; protect the old draft before doing so.
The platform kind starts a new browser storage namespace for drafts and preferences.
Optional CSRF configuration contains names only. The iframe reads the existing cookie for each
service write and rejects missing/duplicate cookies. Platform omits this configuration only for its
server-selected local authentication mode. Credentials never enter messages.
After init, every message carries version, workspaceId, documentId, generation and
requestId. Wait for state with loading: false before opening a document. Use the latest
state's document ID/generation, including any recovered draft; the initial document ID is null.
The workspace rejects stale document generations. A document switch may change both values.
| Host message | Payload in addition to the envelope |
|---|---|
open_profile |
nextDocumentId, optional discard, and either profile: {name, graph, runtime} with optional revision, or templateId. |
open_run |
nextDocumentId, runId, optional discard. History is fetched through the API. |
request_save |
Optional name for profile save-as. Captures the current profile or environment draft. |
save_ack |
The snapshot's request ID and generation, with a saved profile/environment or problem: {code, message, details?}. |
environment_settings |
Environment view only; reports whether opaque host-owned settings are dirty. |
theme |
theme: "light" or "dark". |
navigate |
action: "profiles", "runs" or "leave", optional discard. |
templateId: "blank" creates an empty profile using the existing editor factory; otherwise use a
bootstrap template's id (or name when it has no ID). Template opens cannot carry a profile or
revision. New/imported full documents omit revision. Platform never constructs graph bindings.
Dirty or unresolved drafts refuse switches with unsaved_changes; an explicit host confirmation
may resend with discard: true. Pending saves refuse switches even with discard. The host waits
for the correlated state acknowledgement before changing outer navigation or removing the frame.
save_snapshot replies contain the captured authority, the original request/document/generation
envelope, and either profile: {name, graph, runtime, expectedRevision} or
environment: {id?, name, definition, expectedRevision}. Valid focused field edits commit before
capture; unresolved edits return a structured problem. The host performs the conditional write and
returns save_ack. An acknowledgement only settles its captured snapshot, preserving later edits.
A profile save-as name drops the old revision.
Unsolicited state messages report dirty, pending, validation, saving, loading and profile
name. Command replies report accepted or a structured problem. navigate messages from the
workspace request the host's save or defaults action. The host keeps selection and outer menus.
Set view: "environments" and an explicit environmentApi collection URL in init to mount the hosted environment manager;
readOnly: true disables mutation controls. The server remains responsible for write authorization.
The manager reuses the same theme, state, and guarded navigation messages, with one document identity
for its mounted surface. It handles resource reads and deletion through the service below. Saves emit
only the generic environment snapshot; the host may combine it atomically with private metadata before
acknowledging it. A single opaque dirty bit brings that metadata into the manager's existing navigation
guard without exposing its shape. Profile-specific bridge commands are unavailable. Failed writes
preserve the draft and saved revision.
apiBase must be a same-origin absolute path ending with /. The embedded workspace uses these
paths beneath it; its profile load/save work passes through the host bridge instead:
| Request | Response |
|---|---|
GET environmentApi |
{environments: [{id, name, revision}]}. |
GET environmentApi/{id} |
{id, name, definition, revision}. |
POST environmentApi |
Host save of {id?, name, definition, expectedRevision: string or null} plus host-owned fields; returns the saved resource. |
DELETE environmentApi/{id} |
Delete with {expectedRevision}; returns {deleted: true}. |
GET bootstrap |
Existing workspace bootstrap with templates, workers and runtime schema. |
POST validate |
Native validation of {graph, runtime}. |
POST authoring, POST data |
Native draft transform of {graph, runtime, action}. |
GET runs/{id} |
Version 1 admitted definition with projection version 1. |
GET runs/{id}/history?after=CURSOR |
Existing ordered native history page. |
GET runs/{id}/events?after=CURSOR |
SSE history pages and structured history_error problems. |
Environment writes carry the pinned X-Zeroshot-Workspace bootstrap identity. The generic hosted
manager preserves the explicit collection URL's query parameters for reads, deletion, and draft
recovery; the host owns authorization, persistence, and any additional resource fields.
It reports opaque resourceId/resourceRevision state changes so host controls can refresh their
catalog. Plain local services never create this adapter. Profiles reject runtime.environment.
Committed drafts and their original CAS baseline recover in the same authority and collection
context. Save, delete, and explicit discard clear recovery. Storage errors or size overflow warn
that recovery is unavailable; accepted runs retain their already resolved environment snapshot.
The shared fetch SSE reader retains HTTP/stream problem codes and details, reconnects network
interruptions using the last accepted Last-Event-ID, and cancels readers/timers on disposal.
Frames have an 8 MiB browser limit. HTTP denials and invalid history stop automatic reconnect.
Definition wrappers and pages may carry
observation: {state: "active" | "collecting" | "complete" | "incomplete" | "expired" | "unavailable", code?}.
This metadata describes retained observation; current run termination remains separate.
A finished run can still follow active or collecting observation. Completion closes the stream
only after page.complete drains the observed head. Platform retains these execution cursors and native
control records across archive handoff; Platform status cursors never enter graph playback.
Start with the UI tests and native service tests:
npm --prefix ui test
npm --prefix ui run build
cargo test -p zeroshot --features ui profile_ui --libFor native UI/server changes, also run the affected crate with the feature enabled:
cargo clippy -p zeroshot --all-targets --features ui -- -D warnings
cargo test -p zeroshot --features ui # Unix; Windows: powershell -NoProfile -File scripts/test-windows.ps1 -UiDefault Cargo checks omit ui. Browser scenarios and evidence:
example processes, replay,
live runs, and standalone lifecycle.