Skip to content

feat(dashboard): Workspaces tab β€” live grid, built-in widgets, Overview-as-data [2/3]#101097

Closed
100yenadmin wants to merge 6 commits into
openclaw:mainfrom
100yenadmin:up/pr2-workspaces-ui
Closed

feat(dashboard): Workspaces tab β€” live grid, built-in widgets, Overview-as-data [2/3]#101097
100yenadmin wants to merge 6 commits into
openclaw:mainfrom
100yenadmin:up/pr2-workspaces-ui

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

πŸ“¦ Part of the modular-dashboard package β€” master hub #101136 (architecture diagrams Β· bottom-up merge order Β· the full per-PR test map).

πŸ§ͺ Try it: this is the foundation (Workspaces UI + builtins). Check out #101098 to run the whole feature in one branch; the default Overview-as-data workspace renders on first load.


Control UI: Workspaces tab β€” live grid, built-in widgets, Overview-as-data (2/3)

The dashboard your agent builds with you. This is where the workspace document from #101094 becomes the whole trusted dashboard experience: a live tab strip, a drag/drop grid, nine built-in widgets, and a default Overview that's no longer hand-coded markup but the same document everything else in this series composes β€” all live, no reload, ever.

End-to-end: the Overview-as-data default workspace, then an agent composing a Financials tab live with no reload, a pending custom widget approved and mounted, live data pushes, and the operator dragging, collapsing, and using the cell menu β€” all against the same document

β–Ά Full-quality recording (light / dark).

Stack note. This is #101097 of a three-PR stack (#101094 backend β†’ #101097 here β†’ #101098 custom widgets). The reviewable new work in this PR is the UI layer β€” review #101094 first. Because we don't have upstream write access to set GitHub-native stacked bases, all three PRs target main with cumulative diffs, so this branch's diff includes #101094's commits until it merges to main; read the new ui/ files and the five small core edits below as the delta.

What problem this solves

#101094 gives the document and the control plane; this PR gives operators somewhere to see it, touch it, and use it on day one. Small-business operators need drag/drop, hide/collapse, and a layout that's actually useful the moment they open it β€” no widget-building required. Maintainers need the same operational picture the current Overview page gives them (cost, sessions, cron, activity, instance health), just now expressed as workspace content they can rearrange and extend, and they need to see, live, when an agent has changed something β€” with the ability to immediately push back with their own drag/drop, on the exact same document.

Shipping the grid and the content together, in one PR, is the point: an empty grid isn't a product, and a set of widgets with nowhere to live isn't either. This is the PR where the default Control UI experience actually changes.

What's in this PR

The shell β€” tab strip, grid, interaction:

  • One new Control UI tab, "Workspaces" β€” registered through the plugin-tab descriptor + BUNDLED_TAB_VIEWS mechanism feat(logbook): automatic work journal plugin with Control UI timeline tabΒ #99930 just introduced for logbook. This is the second consumer of that seam, not a new one.
  • Live workspace tab strip rendered from workspace.json, honoring saved tab order, with hidden tabs in an overflow menu. Deep-linkable (?plugin=dashboard&id=workspaces&ws=<slug>) β€” back/forward and reload preserve the active tab.
  • 12-column CSS grid with hand-rolled pointer-based drag/drop and resize (no new grid dependency) β€” drag a widget, see a ghost preview snapped to the grid, drop, and the layout persists via the gateway RPC with optimistic local update + revert-on-failure.
  • Collapse / hide / remove / move-to-tab from each widget's cell menu, all persisted through the same RPC surface feat(dashboard): workspace document + control plane (plugin backend) [1/3]Β #101094 shipped.
  • Per-cell error boundaries β€” if a widget throws, only that cell shows an error card; the rest of the grid, and every other tab, is unaffected.
  • Provenance chips β€” widgets created by an agent show a small "AI" badge with the agent id on hover. This is a feature, not an afterthought: operators should see what their agent built, at a glance.
  • Live updates β€” the view subscribes to plugin.dashboard.changed and refetches (or falls back to a short poll if push events aren't available on a given connection path β€” the PR states explicitly which one shipped).

The content β€” nine built-in widgets, and a real default workspace:

  • 9 built-in, trusted widgets, rendered as Lit templates directly inside the Control UI (not sandboxed β€” these ship with the plugin, they're not agent-authored code):
    • stat-card β€” big number + label + optional delta
    • markdown β€” static or file-bound markdown body
    • table β€” compact rows + count footer
    • iframe-embed β€” embedded URL, respecting the existing embed-sandbox and external-URL policy exactly as chat embeds do today
    • sessions β€” latest sessions with live-run indicator
    • usage β€” cost/tokens mini-summary
    • cron β€” next runs + last status per job
    • instances β€” connected instances + health
    • activity β€” recent activity feed
  • Overview-as-data: the default main workspace ships as a seeded workspace.json (createdBy: "system") β€” usage + instance-health cards, sessions + cron side by side, activity feed below. It reads the same at 1280px and reflows to single-column below ~900px. This replaces the old hand-coded Overview page with the same information, expressed as workspace content instead of markup.
  • Each widget is a thin re-implementation against existing read RPCs (sessions/usage/cron/instances/activity) β€” deliberately not reusing the current page view functions, which are welded to their own pages' state. Every RPC method a builtin can call is frozen into an explicit allowlist, enforced at schema-validation time in feat(dashboard): workspace document + control plane (plugin backend) [1/3]Β #101094's store.
  • Loading / empty / error states on every widget, inside its own cell β€” nothing here can throw past the per-cell boundary this PR's shell provides.
  • Polling hygiene: refresh piggybacks on the document-change broadcast for layout edits, plus a modest per-widget interval for data refresh that stops the moment the tab isn't visible (no orphan timers).

Shell and content share one lifecycle here on purpose: the grid's error boundary is what makes the widget set safe to ship as "always on," and the widget set is what makes the grid worth landing as the new default rather than an opt-in extra.

See it

Overview-as-data β€” the default workspace, light and dark:

Overview-as-data default workspace, light theme β€” nine live builtins: cost and token cards, instance health, sessions, cron ok/error, and the activity feed, all reading live RPCs

The same Overview-as-data workspace in the dark theme, adopting the #99289 palette without any per-theme markup

An agent composing a tab live, and human interaction on the composed grid:

An agent composing a Financials tab live in an already-open browser β€” overlay chips show the dashboard.tab.create / dashboard.widget.add RPCs firing; the tab strip updates with no reload

The composed dashboard under human control β€” a widget cell's kebab menu open (hide / remove / move-to-tab / collapse), escaping its card cleanly over the 12-column grid

How it works

The whole surface is one bundled Control-UI tab. There is no new UI mechanism here β€” this is the second consumer of the plugin-tab seam #99930 introduced for logbook.

From plugin descriptor to a native bundled view

extensions/dashboard registers a single tab descriptor:

api.session.controls.registerControlUiDescriptor({
  surface: "tab", id: "workspaces", label: "Workspaces",
  group: "control", requiredScopes: ["operator.read"],
});

That descriptor is projected into the per-connection hello controlUiTabs payload (scope-filtered, so the tab is hidden for connections lacking operator.read). The Control UI already knows how to render a bundled view for a plugin tab: this PR adds one entry to the BUNDLED_TAB_VIEWS map in plugin-page.ts keyed "dashboard/workspaces", whose lazy import returns our render + stop functions. No descriptor path is set, so the descriptor's generic iframe branch never triggers β€” the tab renders as first-party Lit, not a sandboxed page.

The page itself follows workboard's three-way split, because the controller is stateful: a thin LitElement page (@consume(applicationContext) + subscription lifecycle), a pure dashboard-view.ts render layer, and a lib/dashboard/ controller that owns data and grid logic.

The 12-column grid and hand-rolled drag/drop

The grid is plain CSS grid, 12 columns, widgets absolutely placed by grid-column/grid-row spans from their doc coordinates. Drag/drop and resize are hand-rolled pointer-event math in lib/dashboard/grid.ts β€” deliberately no gridstack/interact.js, per the repo's lean-deps rule, because snap-to-grid and a ghost preview are simple enough not to justify a dependency. A drag shows a ghost snapped to the grid; on drop the layout updates optimistically and persists via dashboard.widget.move / dashboard.widget.setLayout; an RPC failure reverts the local state. Overlapping drops are rejected with a nearest-free-slot fallback rather than silently stacking cells.

Collapse, hide, remove, and move-to-tab all live in each cell's kebab menu and persist through the same #101094 RPC surface β€” this PR adds no new mutation path, only a renderer and an interaction layer over the existing store.

Live updates and the per-cell error boundary

The view subscribes to plugin.dashboard.changed on the gateway client and refetches on a version bump (skipping its own echo) β€” the push path is what shipped (the Control UI's WS client surfaces event frames), with a short poll retained only as a fallback. This is what makes the agent's edits appear live in an already-open browser with no reload.

Every widget render is wrapped in a per-cell error boundary: if a widget throws, only that cell shows an error card β€” the rest of the grid, and every other tab, is untouched. That boundary is precisely what makes it safe to ship the widget set as always-on rather than opt-in, and it's what PR3's sandboxed custom widgets mount inside.

Builtins bind to read RPCs through an allowlist

The 9 builtins (stat-card, markdown, table, iframe-embed, sessions, usage, cron, instances, activity) are trusted first-party Lit templates β€” there is no sandboxing story here because nothing is agent- or user-authored HTML (that's PR3). Each data widget is a thin re-implementation against an existing read RPC, deliberately not reusing the current page view functions, which are welded to their own pages' state. The RPCs a builtin may call are frozen into an explicit allowlist, enforced at schema-validation time in #101094's store β€” a widget gets a named door, not an open one. iframe-embed is the only widget that touches an external URL, and it reuses the existing embed-sandbox / external-URL config the Control UI already enforces for chat embeds β€” no new policy surface.

Overview-as-data

The default main workspace ships as a seeded workspace.json (createdBy: "system"): usage + instance-health cards, sessions and cron side by side, activity below. It reflows to single-column under ~900px. This replaces the old hand-coded Overview page with the same information expressed as workspace content β€” so the redesign goals from #77774 / #86460 become arrangeable content instead of a second hand-coded page.

The core footprint β€” evidence of a lean core

The entire diff to core ui/ is five existing files, all small additive edits:

  • ui/src/pages/plugin/plugin-page.ts β€” one BUNDLED_TAB_VIEWS entry;
  • ui/src/styles.css β€” one stylesheet import;
  • a breadcrumb-label prop threaded through ui/src/app/app-host.ts, ui/src/components/app-topbar.ts, and ui/src/components/dashboard-header.ts (a generic improvement β€” every plugin tab, e.g. logbook, gets a proper breadcrumb label, not just this one).

Everything else is new files under ui/src/pages/plugin/dashboard-*, ui/src/lib/dashboard/, and ui/src/components/dashboard-widget-cell.ts. No new UI framework, no new grid library, no new npm dependency β€” the Lit shell stays exactly as it is.

Real behavior proof

Behavior or issue addressed: The Control UI is a fixed set of hand-coded views β€” an operator (or an agent acting for them) cannot arrange their own dashboard. This PR adds the bundled "Workspaces" Control-UI tab: a live 12-column grid with hand-rolled drag/drop and resize, 9 trusted built-in widgets, and the Overview-as-data default workspace, all rendered from workspace.json and live-updating on plugin.dashboard.changed with no reload.

Evidence: 246 dashboard unit/browser tests pass (grid math including overlap-rejection with nearest-free-slot fallback, the workboard-style controller/lib split, widget-cell chrome + the per-cell error boundary), plus the mocked-gateway E2E scenarios and a clean pnpm ui:build. The rendered result β€” Overview-as-data in light and dark, an agent composing a Financials tab live with the dashboard.* RPCs firing, and human drag/collapse/menu interaction on the composed grid β€” is in the screenshots above and the recording on the tracking issue.

Verification

Size β€” why the diff looks big but isn't: GitHub shows ~14K changed lines, but the reviewable source is ~4,000 lines across 23 files. The remainder is 61 generated i18n locale-bundle files (regenerated by pnpm ui:i18n:sync, never hand-edited β€” English source lives in ui/src/i18n/locales/en.ts) plus tests. The hand-written UI-source increment is well under VISION.md's ~5K reviewable-line guideline.

  • Unit tests for grid math (snap-to-grid, overlap rejection with nearest-free-slot fallback) and the dashboard controller/lib split.
  • Component/browser tests for the widget cell (collapse, menu actions, error-boundary rendering).
  • Data-shape mapping unit tests per widget (RPC payload fixture in, rendered model out).
  • E2E scenarios: drag/resize/collapse/hide persisting through reload, live-update on a broadcast event, the broken-widget isolation case (sibling cells and other tabs unaffected), all 9 builtins with mocked data asserting loading/empty/error states render correctly.
  • iframe-embed sandbox-mode Γ— internal/external URL matrix tested against the existing embed policy.
  • Default-workspace seed test: fresh state dir produces the documented layout; visual screenshot check light+dark.
  • Timer-lifecycle test: polling stops when the Workspaces tab is left (no orphan intervals).
  • pnpm ui:i18n:check and pnpm ui:build clean; no new ui/ dependencies.
  • The dashboard UI suites pass (246 unit/browser tests across lib/dashboard, the plugin page, and the widget cells), plus the mocked-gateway E2E scenarios and ui:build. CI runs on this PR.

Closes / part of

Closes #66138 ("make OpenClaw feel like WordPress" β€” this is the modular, plugin-driven surface that issue asked for). Supersedes #77774 / #86460 (the competing Overview redesign proposals) by making that content a workspace document instead of a second hand-coded page β€” the redesign goals those issues wanted become workspace content, arrangeable like everything else. Partially addresses #72545 (exec-approvals surface β€” activity/cron widgets surface this operational picture, though a dedicated approvals widget is future work) and #76089 (spawn-subagent button β€” the sendPrompt bridge capability lands in #101098, but the sessions/activity widgets here are the visibility half of that story). Part of #101093. Depends on #101094. Precedes #101098.

Eva added 3 commits July 6, 2026 23:57
New bundled plugin `extensions/dashboard`: a gateway-owned workspace
document (atomic JSON store with undo, size caps, schema validation),
14 `dashboard.*` gateway methods with change broadcasts, an
`openclaw dashboard` CLI, and 14 `dashboard_*` agent tools β€” all sharing
one validated store so humans, the CLI, and agents mutate the dashboard
through the same guarded path. Layout is data; no core changes.

Part of the modular-dashboard series (backend layer).
… (PR2)

The Workspaces bundled tab view for the dashboard plugin: tab strip, grid
with pointer drag/resize/collapse, the 9 builtin widgets + Overview-as-data
default workspace, and the UX polish (themed dialogs, page header, onboarding,
humane error cards, skeletons, (custom) suffix strip).

Wires the plugin's Control UI descriptor (registerControlUiDescriptor) and the
BUNDLED_TAB_VIEWS "dashboard/workspaces" entry, plus the #6 breadcrumb label
(app-host -> app-topbar -> dashboard-header currentLabel) so "Plugin" reads
"Workspaces".

Custom-widget (custom:<name>) rendering is the sandboxed-host feature landing in
the follow-up PR: this layer ships the TRUSTED widget-cell (builtin dispatch +
neutral placeholder for custom kinds, no iframe) and the view without the
manifest/custom-host plumbing, so it builds with no L5 files present.

i18n: en.ts carries all dashboard.* keys; non-English bundles regenerated with
English fallbacks (translation pass runs before the PR opens).
…n TM)

Reuses existing dashboard.* translations from the integration branch's
translation memory instead of an English-fallback sync, so the new
Workspaces Control UI keys ship translated in all 20 locales without
any LLM calls.
@100yenadmin
100yenadmin requested a review from a team as a code owner July 6, 2026 18:18
@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@github-actions github-actions Bot added the dependencies-changed PR changes dependency-related files label Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Dependency Guard

This PR changes dependency-related files. Maintainers should confirm these changes are intentional.

Changed files:

  • extensions/dashboard/package.json
  • pnpm-lock.yaml

Maintainer follow-up:

  • Review whether the dependency changes are intentional.
  • Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.
  • Treat package-lock.json and npm-shrinkwrap.json diffs as security-review surfaces.
  • Run pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json locally for detailed release-style evidence.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Dependency graph changes are blocked

OpenClaw does not accept dependency graph changes through PRs unless a repository admin or security explicitly authorizes the current head SHA. Dependency updates are generated internally by maintainers so external PRs cannot change the resolved graph.

Detected dependency graph changes:

  • pnpm-lock.yaml changed.
  • extensions/dashboard/package.json changed dependencies, devDependencies, peerDependencies, peerDependenciesMeta, name, version.

Auto-scrub was not attempted because this PR changes package manifest dependency graph fields:

  • extensions/dashboard/package.json changed dependencies, devDependencies, peerDependencies, peerDependenciesMeta, name, version.

Dependency graph changes must be reviewed by security or handled by maintainers internally. Please remove lockfile changes manually if they are not needed.

To remove lockfile changes, restore them from the target branch:

git fetch origin
git checkout 'origin/main' -- 'pnpm-lock.yaml'
git commit -m 'chore: remove dependency lockfile change'
git push

If this PR intentionally needs a dependency graph change, ask a repository admin or member of @openclaw/openclaw-secops to comment:

/allow-dependencies-change

The action will approve the current head SHA (78e2a8facfa54857db55e8cad1834f4d7e1e58be) when it reruns. A later push requires a fresh approval.

@100yenadmin

Copy link
Copy Markdown
Contributor Author

Dependency-graph note: this PR inherits #101094's lockfile change (the new extensions/dashboard importer + [email protected], already in the resolved graph). See #101094 for the full breakdown β€” no new packages enter the graph.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 10, 2026, 3:25 AM ET / 07:25 UTC.

Summary
The branch adds a default-enabled dashboard plugin and bundled Workspaces Control UI with persistent layouts, live grid mutations, nine built-in widgets, data bindings, seeded Overview content, and generated localization updates.

Reproducibility: yes. for the patch defects: current-head source deterministically sends payloads rejected by its own gateway handlers and omits the persisted hidden flag from the UI model. The proposal itself is a new feature rather than a current-main bug.

Review metrics: 2 noteworthy metrics.

  • Persistent state surfaces: 1 workspace JSON plus a 20-entry JSON undo ring. The new steady-state files conflict with the repository's SQLite-only runtime-state architecture and create migration obligations.
  • Default runtime surfaces: 1 startup-enabled plugin and 14 agent tool contracts. Existing installs gain a new active plugin and agent capability surface on upgrade.

Stored data model
Persistent data-model change detected: database schema: extensions/dashboard/src/default-workspace.ts, database schema: extensions/dashboard/src/schema.ts, database schema: ui/src/lib/dashboard/index.test.ts, database schema: ui/src/pages/plugin/dashboard-view.test.ts, serialized state: extensions/dashboard/src/cli.ts, serialized state: extensions/dashboard/src/data-read.test.ts, and 7 more. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #101093
Summary: This PR is the Workspaces UI implementation candidate for the canonical modular-dashboard proposal, cumulatively includes the backend PR, and is itself included by the custom-widget follow-up.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: πŸ¦ͺ silver shellfish
Proof: 🦐 gold shrimp
Patch quality: πŸ¦ͺ silver shellfish
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix the request-shape and hidden-widget defects with real request-path coverage.
  • Move workspace and undo persistence to plugin KV or shared SQLite.
  • Resolve conflicts and post inspectable exact-head browser proof after the product decision.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR links light and dark recordings, but prepared inspection failed because ffprobe was unavailable and the current source still contradicts the claimed mutation and file-binding behavior; after fixes, update the PR body with redacted exact-head proof to trigger re-review.

Risk before merge

  • [P1] The enabled-by-default plugin adds startup behavior, persistent state, and 14 agent tools to existing installations without settled upgrade or rollback behavior.
  • [P1] The conflicting stacked branch needs a fresh three-way merge review, especially around generated i18n and current Control UI shell changes.
  • [P1] The external package and lockfile change remains blocked unless removed or explicitly approved for the exact head.
  • [P1] Real browser proof must be repeated after the source blockers and conflict resolution; current recordings cannot validate behavior the current source rejects.

Maintainer options:

  1. Fix and narrow to opt-in (recommended)
    Repair the source defects, migrate state to SQLite, disable default activation, preserve Overview, and return with exact-head proof.
  2. Accept the default change intentionally
    Maintain enabled-by-default behavior only after explicit product ownership and demonstrated fresh-install, upgrade, and rollback behavior.
  3. Pause the series
    Close or pause this PR if maintainers do not want this architecture or dashboard direction in the bundled product.

Next step before merge

  • [P1] A maintainer must first decide the default product direction and storage owner; the contributor also needs to fix the concrete request-contract and hidden-state defects.

Maintainer decision needed

  • Question: Should OpenClaw ship this composable dashboard as an enabled-by-default bundled surface, and should it coexist with or replace the current Overview experience?
  • Rationale: The PR changes startup behavior, persisted state, agent tool exposure, and the primary Control UI direction; code review cannot safely choose that product and upgrade boundary.
  • Likely owner: steipete β€” Recent merged history establishes this handle as the strongest available owner for the Control UI direction and bundled plugin-tab surface.
  • Options:
    • Ship opt-in first (recommended): Land only after correctness and SQLite fixes with the plugin disabled by default and the current Overview preserved.
    • Approve as default: Explicitly approve startup activation and define Overview replacement or coexistence with fresh-install, upgrade, rollback, and support proof.
    • Keep outside core: Pause this stack and pursue the composable dashboard as an external plugin if it should not become a bundled product surface.

Security
Cleared: No concrete security or supply-chain regression was found in this PR layer; the remaining blockers are correctness, storage architecture, compatibility, and product direction.

Review findings

  • [P1] Match the gateway widget mutation contract β€” ui/src/lib/dashboard/index.ts:546
  • [P1] Send file bindings under the binding parameter β€” ui/src/lib/dashboard/index.ts:705-708
  • [P2] Preserve the widget hidden field in the UI model β€” ui/src/lib/dashboard/index.ts:168-176
Review details

Best possible solution:

Use plugin KV or the shared SQLite state database for workspace and undo history, align every UI request with the gateway contract, retain the persisted hidden flag, preserve the current Overview and make the plugin opt-in until maintainers explicitly approve a new default, then rebase and provide exact-head browser proof.

Do we have a high-confidence way to reproduce the issue?

Yes for the patch defects: current-head source deterministically sends payloads rejected by its own gateway handlers and omits the persisted hidden flag from the UI model. The proposal itself is a new feature rather than a current-main bug.

Is this the best way to solve the issue?

No. Reusing the bundled plugin-tab seam is appropriate, but the present implementation breaks central mutation and file-binding contracts, loses hidden state, violates the SQLite storage architecture, and assumes an unapproved default product direction.

Full review comments:

  • [P1] Match the gateway widget mutation contract β€” ui/src/lib/dashboard/index.ts:546
    The gateway only accepts tab, id, and either grid/toTab or a nested patch, but these helpers send slug, widgetId, toSlug, and top-level update fields. readParams rejects those keys, so drag, resize, collapse, rename, hide, and move-to-tab all revert instead of persisting. This remains unresolved from the prior review.
    Confidence: 0.99
  • [P1] Send file bindings under the binding parameter β€” ui/src/lib/dashboard/index.ts:705-708
    dashboard.data.read accepts only { binding }, while this request sends { path, pointer }; every file-backed markdown, table, or stat binding therefore fails before resolveBinding can read the file. This remains unresolved from the prior review.
    Confidence: 0.99
  • [P2] Preserve the widget hidden field in the UI model β€” ui/src/lib/dashboard/index.ts:168-176
    The stored widget schema includes hidden, but the UI type and normalizer drop it. Once the document is refetched, a hidden widget cannot remain excluded from the grid and effectively reappears. This remains unresolved from the prior review.
    Confidence: 0.98
  • [P1] Store dashboard runtime state in SQLite β€” extensions/dashboard/src/store.ts:48-50
    Late finding: this file was unchanged at the earlier reviewed head, but the repository's current hard policy requires OpenClaw-owned runtime state and history to use SQLite or plugin KV. Creating workspace.json plus a directory of undo JSON snapshots adds a prohibited steady-state file store and should be redesigned before merge.
    Confidence: 0.97
    Late finding: first raised on code an earlier review cycle already covered.

Overall correctness: patch is incorrect
Overall confidence: 0.97

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against fc507c70c5e3.

Label changes

Label changes:

  • add rating: πŸ¦ͺ silver shellfish: Overall readiness is πŸ¦ͺ silver shellfish; proof is 🦐 gold shrimp and patch quality is πŸ¦ͺ silver shellfish.
  • remove rating: πŸ§‚ unranked krab: Current PR rating is rating: πŸ¦ͺ silver shellfish, so this older rating label is no longer current.
  • remove merge-risk: 🚨 security-boundary: Current PR review merge-risk labels are merge-risk: 🚨 compatibility.
  • remove proof: πŸŽ₯ video: Current real behavior proof evidence kind is linked_artifact.

Label justifications:

  • P2: This is a substantial new Control UI capability with meaningful product impact but no urgent current-user regression.
  • merge-risk: 🚨 compatibility: Merging changes default activation, persistent state, tool exposure, and the dashboard experience for existing installations.
  • rating: πŸ¦ͺ silver shellfish: Overall readiness is πŸ¦ͺ silver shellfish; proof is 🦐 gold shrimp and patch quality is πŸ¦ͺ silver shellfish.
  • status: πŸ“£ needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR links light and dark recordings, but prepared inspection failed because ffprobe was unavailable and the current source still contradicts the claimed mutation and file-binding behavior; after fixes, update the PR body with redacted exact-head proof to trigger re-review.
Evidence reviewed

What I checked:

  • Widget mutation contract mismatch: The UI sends slug, widgetId, and top-level update fields, while the gateway accepts tab, id, and a nested patch; readParams rejects the UI payload before persistence. (ui/src/lib/dashboard/index.ts:546, 78e2a8facfa5)
  • File-binding contract mismatch: The UI calls dashboard.data.read with path and pointer, but the gateway only accepts a single binding field, so file-backed widgets return an error. (ui/src/lib/dashboard/index.ts:705, 78e2a8facfa5)
  • Hidden widget state lost: The canonical schema persists hidden, but the UI widget type and normalizer omit it, so a refetch cannot preserve the hidden state needed to keep a hidden widget out of the rendered grid. (ui/src/lib/dashboard/index.ts:168, 78e2a8facfa5)
  • Runtime storage architecture conflict: DashboardStore creates steady-state dashboard/workspace.json and JSON undo snapshots, while repository policy requires OpenClaw-owned runtime state, caches, histories, and plugin scratch data to use SQLite or plugin KV. (extensions/dashboard/src/store.ts:48, 78e2a8facfa5)
  • Re-review continuity: The dashboard files behind all three previous findings are unchanged between the prior reviewed SHA and the current head, so those findings remain unresolved rather than fixed by the later upstream merges. (ui/src/lib/dashboard/index.ts:538, 78e2a8facfa5)
  • Live branch state: GitHub reports the current head as conflicting with main; the dependency graph guard also still requires the external contributor's package and lockfile change to be removed or explicitly approved. (extensions/dashboard/package.json:7, 78e2a8facfa5)

Likely related people:

  • steipete: Recent merged history introduced the bundled plugin-tab registry and Control UI navigation/default patterns that own this PR's integration and product boundary. (role: introduced and recently maintained adjacent UI seam; confidence: high; commits: c730d8f1f1bb, bb724436150e, 0e4c7cd155; files: ui/src/pages/plugin/plugin-page.ts, ui/src/app/app-host.ts)
What the crustacean ranks mean
  • πŸ¦€ challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • πŸ¦ͺ silver shellfish: thin signal; proof, validation, or implementation needs work.
  • πŸ§‚ unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (2 earlier review cycles)
  • reviewed 2026-07-06T19:31:57.780Z sha 2747048 :: needs real behavior proof before merge. :: [P1] Use the gateway's widget mutation parameter shape | [P1] Send file bindings under dashboard.data.read binding | [P2] Keep hidden widgets out of the rendered read model
  • reviewed 2026-07-08T04:27:17.799Z sha 2747048 :: needs real behavior proof before merge. :: [P1] Use the gateway's widget mutation parameter shape | [P1] Send file bindings under dashboard.data.read binding | [P2] Preserve hidden widgets in the UI model

Eva added 3 commits July 10, 2026 06:13
# Conflicts:
#	ui/src/app/app-host.ts
#	ui/src/components/app-topbar.ts
#	ui/src/components/dashboard-header.ts
#	ui/src/i18n/.i18n/ar.meta.json
#	ui/src/i18n/.i18n/ar.tm.jsonl
#	ui/src/i18n/.i18n/de.meta.json
#	ui/src/i18n/.i18n/de.tm.jsonl
#	ui/src/i18n/.i18n/es.meta.json
#	ui/src/i18n/.i18n/es.tm.jsonl
#	ui/src/i18n/.i18n/fa.meta.json
#	ui/src/i18n/.i18n/fa.tm.jsonl
#	ui/src/i18n/.i18n/fr.meta.json
#	ui/src/i18n/.i18n/fr.tm.jsonl
#	ui/src/i18n/.i18n/hi.meta.json
#	ui/src/i18n/.i18n/hi.tm.jsonl
#	ui/src/i18n/.i18n/id.meta.json
#	ui/src/i18n/.i18n/id.tm.jsonl
#	ui/src/i18n/.i18n/it.meta.json
#	ui/src/i18n/.i18n/it.tm.jsonl
#	ui/src/i18n/.i18n/ja-JP.meta.json
#	ui/src/i18n/.i18n/ja-JP.tm.jsonl
#	ui/src/i18n/.i18n/ko.meta.json
#	ui/src/i18n/.i18n/ko.tm.jsonl
#	ui/src/i18n/.i18n/nl.meta.json
#	ui/src/i18n/.i18n/nl.tm.jsonl
#	ui/src/i18n/.i18n/pl.meta.json
#	ui/src/i18n/.i18n/pl.tm.jsonl
#	ui/src/i18n/.i18n/pt-BR.meta.json
#	ui/src/i18n/.i18n/pt-BR.tm.jsonl
#	ui/src/i18n/.i18n/ru.meta.json
#	ui/src/i18n/.i18n/ru.tm.jsonl
#	ui/src/i18n/.i18n/th.meta.json
#	ui/src/i18n/.i18n/th.tm.jsonl
#	ui/src/i18n/.i18n/tr.meta.json
#	ui/src/i18n/.i18n/tr.tm.jsonl
#	ui/src/i18n/.i18n/uk.meta.json
#	ui/src/i18n/.i18n/uk.tm.jsonl
#	ui/src/i18n/.i18n/vi.meta.json
#	ui/src/i18n/.i18n/vi.tm.jsonl
#	ui/src/i18n/.i18n/zh-CN.meta.json
#	ui/src/i18n/.i18n/zh-CN.tm.jsonl
#	ui/src/i18n/.i18n/zh-TW.meta.json
#	ui/src/i18n/.i18n/zh-TW.tm.jsonl
#	ui/src/pages/plugin/plugin-page.ts
#	ui/src/styles.css
# Conflicts:
#	ui/src/i18n/.i18n/ar.meta.json
#	ui/src/i18n/.i18n/de.meta.json
#	ui/src/i18n/.i18n/es.meta.json
#	ui/src/i18n/.i18n/fa.meta.json
#	ui/src/i18n/.i18n/fr.meta.json
#	ui/src/i18n/.i18n/hi.meta.json
#	ui/src/i18n/.i18n/id.meta.json
#	ui/src/i18n/.i18n/it.meta.json
#	ui/src/i18n/.i18n/ja-JP.meta.json
#	ui/src/i18n/.i18n/ko.meta.json
#	ui/src/i18n/.i18n/nl.meta.json
#	ui/src/i18n/.i18n/pl.meta.json
#	ui/src/i18n/.i18n/pt-BR.meta.json
#	ui/src/i18n/.i18n/ru.meta.json
#	ui/src/i18n/.i18n/th.meta.json
#	ui/src/i18n/.i18n/tr.meta.json
#	ui/src/i18n/.i18n/uk.meta.json
#	ui/src/i18n/.i18n/vi.meta.json
#	ui/src/i18n/.i18n/zh-CN.meta.json
#	ui/src/i18n/.i18n/zh-TW.meta.json
# Conflicts:
#	ui/src/i18n/.i18n/ar.meta.json
#	ui/src/i18n/.i18n/de.meta.json
#	ui/src/i18n/.i18n/es.meta.json
#	ui/src/i18n/.i18n/fa.meta.json
#	ui/src/i18n/.i18n/fr.meta.json
#	ui/src/i18n/.i18n/hi.meta.json
#	ui/src/i18n/.i18n/id.meta.json
#	ui/src/i18n/.i18n/it.meta.json
#	ui/src/i18n/.i18n/ja-JP.meta.json
#	ui/src/i18n/.i18n/ko.meta.json
#	ui/src/i18n/.i18n/nl.meta.json
#	ui/src/i18n/.i18n/pl.meta.json
#	ui/src/i18n/.i18n/pt-BR.meta.json
#	ui/src/i18n/.i18n/ru.meta.json
#	ui/src/i18n/.i18n/th.meta.json
#	ui/src/i18n/.i18n/tr.meta.json
#	ui/src/i18n/.i18n/uk.meta.json
#	ui/src/i18n/.i18n/vi.meta.json
#	ui/src/i18n/.i18n/zh-CN.meta.json
#	ui/src/i18n/.i18n/zh-TW.meta.json
@clawsweeper clawsweeper Bot added rating: πŸ¦ͺ silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed proof: πŸŽ₯ video Contributor real behavior proof includes video or recording evidence. rating: πŸ§‚ unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jul 10, 2026
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
… sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>
steipete added a commit that referenced this pull request Jul 11, 2026
* feat(dashboard): modular dashboard β€” workspace store, Workspaces tab, sandboxed custom widgets

Squashes #101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>

* fix(dashboard): UI review fixes β€” grid, error boundary, embed sandbox, locale

* fix(dashboard): make the CLI and agent broadcasts actually reachable

Three defects only a live run surfaces, all invisible to the unit suites:

- The plugin claimed the CLI command name `dashboard`, which core already owns
  (it opens the Control UI). A plugin CLI group that overlaps a core command is
  dropped at registration behind a `logger.debug`, so the entire CLI face was
  unreachable while `cli.test.ts` kept passing against its own Commander
  program. Renamed to `openclaw workspaces`, matching the tab it drives.

- The manifest never declared `activation.onCommands`, so the CLI root resolved
  to no owning plugin even once the name was free.

- `dashboard.widget.approve` needs `operator.approvals`; the CLI asked for
  `operator.write` on every call. It now requests the approvals scope only for
  the approve call, matching `operator-approvals-client.ts`.

Also: agent tools resolved their broadcast from the plugin runtime's
gateway-request scope, an AsyncLocalStorage set only around gateway RPCs and
plugin HTTP routes. An agent turn started from a channel, cron, or heartbeat
therefore wrote the document without emitting `plugin.dashboard.changed`, so an
open Control UI never saw the edit β€” the feature's headline promise. The gateway
broadcast is server-lifetime, so the plugin now remembers it in a single slot and
agent tools fall back to it.

* docs(web): document dashboard workspaces, provenance, and the custom-widget sandbox

* fix(dashboard): agent-tool ergonomics + close two approval-boundary gaps

From a source-blind agent driving the dashboard_* tools with nothing but their
schemas, and from a Codex review of the hardening delta.

- dashboard_widget_update could never succeed. It passed its whole parameter
  record to the patch reader, whose allowlist rejects the very `tab`/`id` keys
  the tool's own schema marks required, so every call died on
  "unexpected param: tab". Its test only ran Value.Check against the schema and
  never executed the tool.
- dashboard_data_read surfaced an `rpc` binding as a thrown error, though its
  description promised `binding_client_resolved`. It now returns that as a
  result the model can act on.
- Valid widget kinds and the rpc allowlist were undiscoverable: a model saw only
  "builtin:<name> or custom:<name>" and "Allowlisted gateway read method", then
  brute-forced ~40 calls against errors that named no alternatives. Both schemas
  and both validator errors now enumerate them, and the kind description says
  what each builtin renders and which binding id it reads. widget_move documents
  that grid and toTab are exclusive; widget_scaffold says an operator must
  approve, because no agent tool can.
- workspace.replace could mint a pending registry entry for a name that was
  never scaffolded. An operator could then approve a widget whose code did not
  exist yet, and the agent could write it afterwards. Registry entries now come
  from dashboard_widget_scaffold and nowhere else, and approve refuses a name
  with no manifest on disk.
- dashboard.widget.approve answered with the whole workspace document, so a
  connection holding only operator.approvals could read it through the approvals
  door. It now returns the registry entry it changed.

* fix(dashboard): approval pins the code it approves

Codex review found the scaffold-before-approval gate still nameable rather than
binding: approve only proved that widget.json parsed, and the Control UI loaded a
hardcoded index.html rather than the manifest's entrypoint. An agent could
scaffold a widget, win approval on an innocuous or absent entrypoint, then write
the real payload afterwards β€” code appearing after the human said yes.

Approval now hashes every servable file in the widget directory and stores the
digests on the registry entry, refusing a manifest whose declared entrypoint is
missing. The asset route re-hashes each file it reads and 404s anything that does
not match, so a file edited or added after approval never reaches a browser. The
Control UI loads the manifest's entrypoint, which is the file that was hashed.

The content-type allowlist moves to manifest.ts so the set of files approval
hashes and the set the route can serve cannot drift apart.

Proof, against a running gateway: scaffold -> 404, approve -> 200, rewrite
index.html -> 404, add late.js -> 404.

* fix(dashboard): parse the approved manifest from the bytes that were hashed

Codex found a TOCTOU in the approval path: it loaded and validated widget.json,
then walked the directory again to compute the digests. An agent could swap
widget.json between the two reads, so the operator validated one entrypoint while
the digest froze β€” and the Control UI later mounted β€” a different one.

snapshotApprovedWidget now reads the widget directory once: it hashes every
servable file, parses the manifest out of the same widget.json bytes it hashed,
and requires the declared entrypoint to be among them.

Proof, against a running gateway: approve -> index.html 200; rewrite widget.json
to point at evil.html and drop evil.html in -> both 404.

* fix(dashboard): cap approval asset reads; bound the grid fallback search

Two findings from the fourth Codex pass.

Approval hashes agent-authored files that are untrusted until it runs, and read
each one into memory with no size check β€” dropping one huge .png into a scaffold
directory would stall or OOM the gateway during approve. Sizes are now checked
before the read, with a 2 MB per-file and 8 MB total cap.

nearestFreeSlot searched one band below the lowest occupied row, so a crowded
layout near the bottom could return y=500 as the closest free slot: a placement
the store rejects, which the UI applies optimistically and then snaps back. The
search now stops at the last row a widget of that height can legally occupy.

* fix(dashboard): refuse oversized widget assets before reading them

Approved widget files stay writable and the asset route is unauthenticated, so
swapping an approved small file for a very large one made every GET buffer the
whole file before the digest check rejected it. The route now refuses anything
past the same per-file cap approval enforces, on the stat it already performs.

* fix(dashboard): enforce widget approval boundaries

* docs(changelog): note modular dashboard workspaces

* fix(dashboard): enforce static custom-widget data boundary

* fix(dashboard): satisfy UI lint

* test(dashboard): avoid legacy proto access

* feat(dashboard): make plugin opt-in

* docs(dashboard): refresh workspaces map

* refactor(workspaces): standardize plugin naming

* fix(workspaces): make widget prompt sends idempotent

* docs(workspaces): fix internal path references

* test(workspaces): make prompt assertion lint-safe

* test(workspaces): type prompt request mock

* fix(workspaces): harden approval and binding boundaries

* test(workspaces): complete stale binding client mock

* fix(workspaces): harden widget file boundaries

* fix(workspaces): scope custom widget capabilities

* fix(workspaces): align approval provenance

* fix(workspaces): close branch contract gaps

* test(workspaces): complete builtin context fixtures

* fix(workspaces): aggregate overview usage

* chore(workspaces): defer release note

* chore(workspaces): refresh i18n metadata

---------

Co-authored-by: 100yenadmin <[email protected]>
@steipete

Copy link
Copy Markdown
Contributor

Superseded by the integrated implementation in #104139, landed as 0af84671374962f1ca7f8047d655d93d96204ee1.

The Workspaces tab, live grid, built-in widgets, and Overview-as-data scope are now on main in the canonical Workspaces implementation. The landed PR preserves the original product direction and contributor credit and adds exact-head CI plus live screenshot proof.

Closing this stacked UI PR so #104139 remains the canonical implementation. Thank you @100yenadmin for the original work.

@steipete steipete closed this Jul 11, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 12, 2026
* feat(dashboard): modular dashboard β€” workspace store, Workspaces tab, sandboxed custom widgets

Squashes openclaw#101094 + openclaw#101097 + openclaw#101098 onto current main and
applies the maintainer review fixes to the backend control plane.

Co-authored-by: 100yenadmin <[email protected]>

* fix(dashboard): UI review fixes β€” grid, error boundary, embed sandbox, locale

* fix(dashboard): make the CLI and agent broadcasts actually reachable

Three defects only a live run surfaces, all invisible to the unit suites:

- The plugin claimed the CLI command name `dashboard`, which core already owns
  (it opens the Control UI). A plugin CLI group that overlaps a core command is
  dropped at registration behind a `logger.debug`, so the entire CLI face was
  unreachable while `cli.test.ts` kept passing against its own Commander
  program. Renamed to `openclaw workspaces`, matching the tab it drives.

- The manifest never declared `activation.onCommands`, so the CLI root resolved
  to no owning plugin even once the name was free.

- `dashboard.widget.approve` needs `operator.approvals`; the CLI asked for
  `operator.write` on every call. It now requests the approvals scope only for
  the approve call, matching `operator-approvals-client.ts`.

Also: agent tools resolved their broadcast from the plugin runtime's
gateway-request scope, an AsyncLocalStorage set only around gateway RPCs and
plugin HTTP routes. An agent turn started from a channel, cron, or heartbeat
therefore wrote the document without emitting `plugin.dashboard.changed`, so an
open Control UI never saw the edit β€” the feature's headline promise. The gateway
broadcast is server-lifetime, so the plugin now remembers it in a single slot and
agent tools fall back to it.

* docs(web): document dashboard workspaces, provenance, and the custom-widget sandbox

* fix(dashboard): agent-tool ergonomics + close two approval-boundary gaps

From a source-blind agent driving the dashboard_* tools with nothing but their
schemas, and from a Codex review of the hardening delta.

- dashboard_widget_update could never succeed. It passed its whole parameter
  record to the patch reader, whose allowlist rejects the very `tab`/`id` keys
  the tool's own schema marks required, so every call died on
  "unexpected param: tab". Its test only ran Value.Check against the schema and
  never executed the tool.
- dashboard_data_read surfaced an `rpc` binding as a thrown error, though its
  description promised `binding_client_resolved`. It now returns that as a
  result the model can act on.
- Valid widget kinds and the rpc allowlist were undiscoverable: a model saw only
  "builtin:<name> or custom:<name>" and "Allowlisted gateway read method", then
  brute-forced ~40 calls against errors that named no alternatives. Both schemas
  and both validator errors now enumerate them, and the kind description says
  what each builtin renders and which binding id it reads. widget_move documents
  that grid and toTab are exclusive; widget_scaffold says an operator must
  approve, because no agent tool can.
- workspace.replace could mint a pending registry entry for a name that was
  never scaffolded. An operator could then approve a widget whose code did not
  exist yet, and the agent could write it afterwards. Registry entries now come
  from dashboard_widget_scaffold and nowhere else, and approve refuses a name
  with no manifest on disk.
- dashboard.widget.approve answered with the whole workspace document, so a
  connection holding only operator.approvals could read it through the approvals
  door. It now returns the registry entry it changed.

* fix(dashboard): approval pins the code it approves

Codex review found the scaffold-before-approval gate still nameable rather than
binding: approve only proved that widget.json parsed, and the Control UI loaded a
hardcoded index.html rather than the manifest's entrypoint. An agent could
scaffold a widget, win approval on an innocuous or absent entrypoint, then write
the real payload afterwards β€” code appearing after the human said yes.

Approval now hashes every servable file in the widget directory and stores the
digests on the registry entry, refusing a manifest whose declared entrypoint is
missing. The asset route re-hashes each file it reads and 404s anything that does
not match, so a file edited or added after approval never reaches a browser. The
Control UI loads the manifest's entrypoint, which is the file that was hashed.

The content-type allowlist moves to manifest.ts so the set of files approval
hashes and the set the route can serve cannot drift apart.

Proof, against a running gateway: scaffold -> 404, approve -> 200, rewrite
index.html -> 404, add late.js -> 404.

* fix(dashboard): parse the approved manifest from the bytes that were hashed

Codex found a TOCTOU in the approval path: it loaded and validated widget.json,
then walked the directory again to compute the digests. An agent could swap
widget.json between the two reads, so the operator validated one entrypoint while
the digest froze β€” and the Control UI later mounted β€” a different one.

snapshotApprovedWidget now reads the widget directory once: it hashes every
servable file, parses the manifest out of the same widget.json bytes it hashed,
and requires the declared entrypoint to be among them.

Proof, against a running gateway: approve -> index.html 200; rewrite widget.json
to point at evil.html and drop evil.html in -> both 404.

* fix(dashboard): cap approval asset reads; bound the grid fallback search

Two findings from the fourth Codex pass.

Approval hashes agent-authored files that are untrusted until it runs, and read
each one into memory with no size check β€” dropping one huge .png into a scaffold
directory would stall or OOM the gateway during approve. Sizes are now checked
before the read, with a 2 MB per-file and 8 MB total cap.

nearestFreeSlot searched one band below the lowest occupied row, so a crowded
layout near the bottom could return y=500 as the closest free slot: a placement
the store rejects, which the UI applies optimistically and then snaps back. The
search now stops at the last row a widget of that height can legally occupy.

* fix(dashboard): refuse oversized widget assets before reading them

Approved widget files stay writable and the asset route is unauthenticated, so
swapping an approved small file for a very large one made every GET buffer the
whole file before the digest check rejected it. The route now refuses anything
past the same per-file cap approval enforces, on the stat it already performs.

* fix(dashboard): enforce widget approval boundaries

* docs(changelog): note modular dashboard workspaces

* fix(dashboard): enforce static custom-widget data boundary

* fix(dashboard): satisfy UI lint

* test(dashboard): avoid legacy proto access

* feat(dashboard): make plugin opt-in

* docs(dashboard): refresh workspaces map

* refactor(workspaces): standardize plugin naming

* fix(workspaces): make widget prompt sends idempotent

* docs(workspaces): fix internal path references

* test(workspaces): make prompt assertion lint-safe

* test(workspaces): type prompt request mock

* fix(workspaces): harden approval and binding boundaries

* test(workspaces): complete stale binding client mock

* fix(workspaces): harden widget file boundaries

* fix(workspaces): scope custom widget capabilities

* fix(workspaces): align approval provenance

* fix(workspaces): close branch contract gaps

* test(workspaces): complete builtin context fixtures

* fix(workspaces): aggregate overview usage

* chore(workspaces): defer release note

* chore(workspaces): refresh i18n metadata

---------

Co-authored-by: 100yenadmin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui dependencies-changed PR changes dependency-related files merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. rating: πŸ¦ͺ silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: XL status: πŸ“£ needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants