feat(workspaces): add agent-composable Workspaces#104139
Conversation
cf75e95 to
2e0ad1d
Compare
|
Codex review: stale review; fresh review needed. Summary Next step Review history (13 earlier review cycles; latest 8 shown)
|
2e0ad1d to
6fa1b2b
Compare
OpenClaw QA ArtifactsSummaryLive OpenAI gpt-5.4 proof: the agent created and refined its own six-widget workspace through dashboard tools. Screenshots show the resulting dashboard and operator widget controls. Evidence
|
Dependency GuardThis PR changes dependency-related files. Maintainers should confirm these changes are intentional. Changed files:
Maintainer follow-up:
|
Dependency graph changes notedThis PR includes dependency graph changes. The dependency guard is informational because the PR author is a repository admin or a member of
Security review is still recommended before merge when the dependency graph change is intentional. |
6fa1b2b to
6eeffe8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6fa1b2b5d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const onMessage = (event: MessageEvent): void => { | ||
| // IDENTITY accept filter — never compare origin strings (opaque origin = null). | ||
| if (event.source !== iframe.contentWindow) { | ||
| return; | ||
| } | ||
| bridge.handleMessage(event.data); |
There was a problem hiding this comment.
Reject messages after widget iframe navigates away
For an approved custom widget that navigates its own iframe to an external URL, this source-only check still accepts messages from the same contentWindow WindowProxy; because the sandboxed origin is opaque and serve.ts's CSP does not include a navigate-to 'self' restriction, the external page can use the approved bridge grants (static binding values/theme and, when the manifest has prompt:send, prompt dispatch) even though those bytes were not what the operator approved. The bridge needs to revoke/deny after non-widget navigations or the served document needs to be prevented from navigating off the approved widget route.
Useful? React with 👍 / 👎.
| void resolveBinding(client, binding).then((result) => { | ||
| viewState.bindingResults.set(widget.id, result); | ||
| viewState.bindingLoads.delete(widget.id); |
There was a problem hiding this comment.
Ignore stale binding resolutions
When a workspace update or poll tick changes bindingVersion while a previous resolveBinding call is still pending, the older promise can resolve later and repopulate bindingResults for the same widget id after the cache was cleared (and can even overwrite the newer result). This leaves the Workspaces tab showing data from an older binding/doc version until a later refresh; capture key for the request and only write the result if viewState.bindingVersion still matches.
Useful? React with 👍 / 👎.
6eeffe8 to
b4f1edf
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4f1edfa70
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.db.prepare("DELETE FROM undo WHERE version = ?").run(row.version); | ||
| // transact() stamps the next version, so the restored document lands as a | ||
| // forward write rather than a rewind. | ||
| return validateWorkspaceDoc(JSON.parse(row.doc)); |
There was a problem hiding this comment.
Keep widget approvals out of layout undo
When the undo snapshot was captured by dashboard.widget.approve or a later reject, this restores the full widgetsRegistry including status: "approved" and approvedFiles. Because both dashboard.workspace.undo is only operator.write-scoped and the dashboard_undo agent tool calls the same store.undo(), an actor that cannot hold operator.approvals can resurrect a previously approved custom widget after it was rejected, bypassing the separate approval gate. Keep approval state out of layout undo snapshots or require the approval scope for restoring them.
Useful? React with 👍 / 👎.
| await context.client.request("chat.send", { | ||
| sessionKey: context.sessionKey, | ||
| message: text, | ||
| deliver: false, | ||
| }); |
There was a problem hiding this comment.
Include chat.send idempotency keys for widget prompts
For an approved widget with the prompt:send capability, this request is sent only after the operator confirms, but chat.send requires an idempotencyKey in its protocol schema (the normal chat composer passes the run id). Without that field the gateway rejects the call as invalid, so every widget prompt dispatch fails after confirmation instead of sending the prompt.
Useful? React with 👍 / 👎.
728e095 to
a0a02ad
Compare
|
Merged via squash.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0a02ad1d2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const entrypoint = requireString(record, "entrypoint", "widget.json"); | ||
| // The entrypoint is a logical path served through the jail; normalize it the | ||
| // same way the serving route does so a manifest cannot name an out-of-dir file. | ||
| normalizeWorkspaceDataLogicalPath(entrypoint); |
There was a problem hiding this comment.
Reject non-HTML widget entrypoints
When an approved widget declares a servable but non-HTML entrypoint such as widget.svg, index.txt, or app.js, this validation accepts it and approval succeeds, but serveWidgetAsset only injects the openclawWorkspaceBridge bootstrap into text/html responses. The Control UI still mounts the iframe, so any widget that expects the documented bridge/data flow is approved into a permanently nonfunctional frame instead of failing clearly at approval time; require the entrypoint to be an HTML file or make the bootstrap work for every allowed entrypoint type.
Useful? React with 👍 / 👎.
| name, | ||
| title, | ||
| entrypoint: "index.html", | ||
| bindings: [{ id: "value", source: "static", value: "Hello from your workspace widget." }], |
There was a problem hiding this comment.
Make the scaffolded binding usable by default
A scaffolded widget's generated HTML immediately requests the value binding, and this manifest advertises a default static value, but the parent bridge resolves only widget.bindings[bindingId] from the workspace document and ignores the manifest value. If the operator or agent adds custom:<name> without manually duplicating a value binding in the layout, the freshly scaffolded/approved widget renders binding_denied instead of the bundled “Hello” sample; either create/return a matching workspace binding with the scaffold flow or use the manifest static value as the default grant.
Useful? React with 👍 / 👎.


Related: #101093
Supersedes the stacked implementation in #101094, #101097, and #101098 while preserving @100yenadmin's original product direction and contributor credit.
What Problem This Solves
OpenClaw's Control UI is a fixed dashboard: agents cannot assemble a durable workspace around the task they are doing, expose useful live data, or refine that workspace without a code change and redeploy.
Why This Change Was Made
Adds an opt-in, agent-composable Workspaces surface with tabs, built-in widgets, typed agent tools, guarded commands, and an approval flow for custom widgets. Workspace state and provenance live in SQLite; custom widget code remains a named user artifact, is content-pinned at approval, size-bounded, served with a restrictive CSP, and rendered in a sandboxed iframe.
The integration was rebuilt from the original three-PR stack, rebased onto current
main, and hardened through repeated source review and live lifecycle checks. The core boundary stays generic: the bundled Workspaces plugin owns persistence, commands, tools, and widget serving, while Control UI consumes the plugin surface.User Impact
The bundled Workspaces plugin is disabled by default and can be enabled from the Control UI Plugins page or with
openclaw plugins enable workspaces. Once enabled, agents can create and refine task-specific workspaces on demand. Users can inspect provenance, reorder and resize widgets, move them across tabs, approve or reject custom widget proposals, and retain the result across sessions without editing JSON or redeploying OpenClaw.Evidence
openai/gpt-5.4created and refined anOpenAI Liveworkspace with six widgets across five built-in kinds in 46.8 seconds. The resulting workspace persisted in SQLite with no JSON workspace sidecar.a0a02ad1d2b3eea34e0db47bf0f3f931b9ee8413.