Skip to content

feat(dashboard): workspace document + control plane (plugin backend) [1/3]#101094

Closed
100yenadmin wants to merge 3 commits into
openclaw:mainfrom
100yenadmin:up/pr1-backend
Closed

feat(dashboard): workspace document + control plane (plugin backend) [1/3]#101094
100yenadmin wants to merge 3 commits into
openclaw:mainfrom
100yenadmin:up/pr1-backend

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 (backend control plane). Check out the top of the foundation stack — #101098 — to run the whole feature in one branch; then follow the hub's per-PR test map.


dashboard plugin: workspace document + control plane (1/3)

The dashboard your agent builds with you. This PR is the foundation: one guarded document, one store, three faces (gateway RPC, CLI, agent tools) that all funnel through it. No UI yet — that's PR2. This is the plumbing that makes "agent and human edit the same layout" possible at all.

This PR has no UI, but its whole point is what happens on the wire. Here the control plane is doing exactly what it exists to do: an agent's dashboard.tab.create / dashboard.widget.add calls land in the store and compose a live tab (the overlay chips are the real RPCs firing) — the same validated path a human's CLI call takes.

An agent composing a Financials tab through the dashboard.* control plane — overlay chips show the real dashboard.tab.create / dashboard.widget.add RPCs firing against the store

Stack note. This is the base of a three-PR stack (#101094 here → #101097 UI → #101098 custom widgets); each later PR targets main and builds cumulatively on this one. This PR is self-contained — extensions/dashboard only, zero core changes — and independently mergeable/useful on its own.

What problem this solves

Right now there's no shared, structured place for "what does this operator's dashboard look like" to live — any composability has to be hand-coded per view. Two audiences need this: operators who want to lay out their own control hub without touching code, and agents that want to build that layout for them. Both need the same underlying guarantee — one validated document, no partial writes, no path where a human's layout and an agent's layout can silently diverge.

What's in this PR

  • extensions/dashboard — a new bundled plugin, enabledByDefault: true, zero core-file changes (git diff --stat is scoped entirely to the plugin directory).
  • workspace.json — the layout-as-data document (tabs → widgets → grid position → data bindings), owned at <stateDir>/dashboard/workspace.json, schemaVersion-migrated on read.
  • DashboardStore — the single writer. Atomic writes (replaceFileAtomic), async-mutex-serialized mutations, a 256 KB size cap, a 32-tab / 24-widget-per-tab cap, and a 20-entry undo ring. Every mutation is validated by hand-written guards (repo idiom — no schema library at the RPC layer); invalid ops are rejected whole, never partially applied.
  • 14 gateway methods (dashboard.*) covering tab CRUD/reorder, widget CRUD/move/batch-layout, widget approval, full-document replace (bulk agent authoring), undo, and a scoped data-read endpoint — each correctly scoped operator.read or operator.write.
  • Change broadcasts — every successful write fires plugin.dashboard.changed { workspaceVersion, changedTabSlug, actor } so every connected operator UI can live-update. Reads never broadcast; failed writes never broadcast.
  • Data bindings, not fetches — widgets declare a file (path-jailed under <stateDir>/dashboard/data/) or static binding; nothing in this layer lets a widget make an arbitrary network call. (An rpc binding kind exists in the schema for write-time validation, but is resolved client-side by the trusted Control UI in PR2/PR3 — this plugin's gateway handlers cannot dispatch other gateway methods at this HEAD, so dashboard.data.read intentionally does not proxy rpc bindings. Noted here because it shaped the schema.)
  • CLI + agent tools (this PR includes both, since they're thin callers over the same store): openclaw dashboard tabs/widgets/layout … from a terminal, and 14 dashboard_* agent tools with typebox schemas — both call the identical in-process validated path the gateway RPC uses. Agent-originated writes are stamped createdBy: "agent:<id>" from tool context; this is never overridable via tool params.

See it

The image above is this control plane in action. The whole end-to-end series — CLI call, agent tool call, and the UI layers on top — is in the animated demo on the tracking issue and the integration PR.

How it works

The design goal is a single contract: one validated document, one writer, three faces. Humans and agents both compose the operator's dashboard, and they must never be able to diverge — so every mutation, no matter who originates it, goes through the exact same code path.

workspace.json — layout as data

The dashboard's entire state is one JSON document at <stateDir>/dashboard/workspace.json (state dir via resolveStateDir(), default ~/.openclaw). It is layout-as-data: tabs contain widgets, widgets carry a grid position, data bindings, and provenance. Nothing about a workspace lives in code — it's all this document.

{
  "schemaVersion": 1,
  "workspaceVersion": 42,               // monotonic; bumped on every accepted write
  "tabs": [
    {
      "slug": "financials",             // ^[a-z0-9-]{1,40}$ — also the deep-link key
      "title": "Financials",
      "icon": "barChart",
      "hidden": false,
      "createdBy": "agent:main",        // provenance: "user" | "system" | "agent:<id>"
      "widgets": [
        {
          "id": "w_rev_q3",
          "kind": "builtin:stat-card",  // "builtin:<name>" | "custom:<name>"
          "title": "Q3 Revenue",
          "grid": { "x": 0, "y": 0, "w": 4, "h": 2 },   // 12-column grid units
          "collapsed": false,
          "bindings": {
            "value": { "source": "file", "path": "q3.json", "pointer": "/revenue" }
          },
          "props": { "format": "usd" }
        }
      ]
    }
  ],
  "widgetsRegistry": {                  // custom-widget install/approval state (PR3)
    "revenue-chart": { "status": "approved", "approvedBy": "user", "createdBy": "agent:main" }
  },
  "prefs": { "tabOrder": ["main", "financials"] }
}

schemaVersion is migrated on read; future versions are rejected rather than guessed at.

DashboardStore — the sole writer

DashboardStore is the only thing in the codebase that writes this file. Its mutate(fn, { actor }) path is deliberately boring and total:

  1. acquire a single in-process async mutex (serializes every caller in the one gateway process — RPC, CLI, and agent tools alike);
  2. structuredClone the current doc and apply fn to the draft;
  3. run validateWorkspaceDoc(draft) — hand-written guards, no schema library at the RPC layer (repo idiom);
  4. enforce the caps (≤ 256 KB serialized, ≤ 32 tabs, ≤ 24 widgets/tab);
  5. push the previous serialization onto a 20-entry undo ring under <stateDir>/dashboard/undo/;
  6. replaceFileAtomic (temp file + rename) and bump workspaceVersion.

Any validation failure throws before step 5 — rejects are whole; nothing is ever partially applied, and the file on disk is never touched by a rejected write. Because a single process owns the file and the mutex serializes callers, there is no interleaving and no last-writer-wins race.

14 dashboard.* methods, correctly scoped

The store is fronted by 14 gateway methods, each registered with an explicit scope — reads are operator.read, writes are operator.write:

Reads (operator.read) Writes (operator.write)
dashboard.workspace.get dashboard.tab.create · dashboard.tab.update · dashboard.tab.delete · dashboard.tab.reorder
dashboard.data.read dashboard.widget.add · dashboard.widget.update · dashboard.widget.move · dashboard.widget.remove · dashboard.widget.setLayout
dashboard.widget.approve · dashboard.workspace.replace (bulk agent authoring) · dashboard.workspace.undo

Validation is hand-written param readers per method (throwing typed errors), copied from the extensions/workboard idiom rather than a zod layer — so review reads the same as the rest of the repo.

One store, three faces — the same validated path

This is the crux. The CLI and the agent tools are thin callers over the same store instance, not parallel implementations:

  • Gateway RPC dashboard.* — used by the Control UI (PR2) and the CLI.
  • CLI openclaw dashboard tabs/widgets/layout … — a plugin-registered CLI that calls the RPC.
  • Agent tools dashboard_* (14 tools, typebox input schemas) — call the store in-process, hitting the identical validation + broadcast.
  openclaw dashboard tabs create --title Financials   (CLI, actor defaults to "user")
                            │
  dashboard_tab_create  ────┤   (agent tool, actor stamped "agent:<id>" from tool context)
                            │
  dashboard.tab.create  ────┤   (gateway RPC from the Control UI)
                            ▼
                     DashboardStore.mutate()   ← one mutex, one validator, one atomic write
                            ▼
                     workspace.json  +  plugin.dashboard.changed broadcast

A human's openclaw dashboard tabs create --title Financials and an agent's dashboard_tab_create tool call produce byte-identical documents through byte-identical validation — the only difference is the createdBy stamp, which is set from the caller's context (agent:<id> for tools) and can never be overridden via tool params. That is what makes "agent and human edit the same layout" a guarantee rather than a hope.

Change broadcasts

Every successful write ends with opts.context.broadcast("plugin.dashboard.changed", { workspaceVersion, changedTabSlug, actor }), so every connected operator UI can live-update (PR2 subscribes to this). Reads never broadcast; failed writes never broadcast. The payload is intentionally minimal — receivers refetch; the whole document is never shipped in an event.

Bindings, not fetches

Widgets declare data sources; this layer never lets a widget make an arbitrary call. A binding is a file (path-jailed under <stateDir>/dashboard/data/, reusing the canvas logical-path-normalization idiom) or a static literal, resolved by dashboard.data.read. An rpc binding kind also exists in the schema and is validated at write time against DATA_READ_RPC_ALLOWLIST — but it is deliberately not proxied by this plugin's handlers: at this HEAD, plugin gateway-method handlers cannot dispatch other gateway methods (SDK gate gatewayMethodDispatchAllowed), so rpc bindings are resolved client-side by the trusted Control UI over its own authenticated socket (same authz boundary). dashboard.data.read returns a typed binding_client_resolved for them. This is noted here because it shaped the schema, and PR2/PR3 depend on it.

Everything above is extensions/dashboard only — git diff --stat is scoped entirely to the plugin directory. Zero core-file changes.

Real behavior proof

Behavior or issue addressed: There is no shared, validated document for an operator's dashboard layout, so any composability has to be hand-coded per view and a human's layout and an agent's layout can silently diverge. This PR adds extensions/dashboard — one workspace.json document and one DashboardStore fronted by 14 gateway methods, a CLI, and 14 agent tools, so every write (RPC, CLI, or agent tool) funnels through one validated, atomic, undoable path.

Evidence: The extensions/dashboard test suite passes via node scripts/test-projects.mjs extensions/dashboard — store atomicity, the 20-entry undo ring (21st mutation evicts oldest; undo restores exactly), size/count caps, every schema reject path (slug charset, dup slugs, grid overflow, kind pattern, binding union, createdBy pattern), gateway-method registration/scoping, the binding resolver, and a concurrency test proving two mutations serialize under the mutex; tsgo and oxlint clean. The control plane in action — an agent's dashboard.tab.create / dashboard.widget.add RPCs composing a live tab through the store — is the screenshot above and the end-to-end recording on the tracking issue.

Verification

Size: ~3,300 lines of source across 11 files (the backend + control plane, excluding tests) — well inside VISION.md's ~5K reviewable-line guideline. The full PR is 19 files including its test suite.

  • Full unit suite for the store, schema validation (every reject path — slug charset, duplicate slugs, grid overflow, kind pattern, binding union, size/count caps, createdBy pattern — has a dedicated test), gateway method registration/scoping, and the binding resolver.
  • Concurrency test: two concurrent mutations serialize correctly under the mutex.
  • Undo-ring test: the 21st mutation evicts the oldest snapshot; undo restores exactly the prior document.
  • The extensions/dashboard test suite (store, schema, gateway methods, CLI, agent tools, binding resolver) and tsgo/oxlint pass. CI runs on this PR.

Closes / part of

Part of #101093. Foundation for #101097, #101098 — neither later PR is reachable without this one merged first.

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).
@100yenadmin
100yenadmin requested a review from a team as a code owner July 6, 2026 18:12
@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 added the dependencies-changed PR changes dependency-related files label Jul 6, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jul 6, 2026
@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 (500fadef347bd0281a2c590a826d8bdfb5e16e9f) when it reruns. A later push requires a fresh approval.

@100yenadmin

Copy link
Copy Markdown
Contributor Author

Note for maintainers — the dependency-guard red is the expected new-plugin footprint

This PR adds a new bundled plugin (extensions/dashboard), which is why dependency-guard flags a lockfile change. To make authorization easy, here's the exact, minimal delta:

  • No new package enters the resolved graph. The plugin's only runtime dependency is typebox, pinned to 1.3.3 — the exact version already resolved in pnpm-lock.yaml and already depended on by extensions/workboard, extensions/canvas, extensions/codex, extensions/browser, extensions/diffs, and others.
  • No version bumps, no transitive-graph changes. The only lockfile delta is the new extensions/dashboard workspace-importer entry — the standard, unavoidable footprint of adding any bundled plugin.

So the dependency graph is effectively unchanged; the guard fires purely because pnpm-lock.yaml / package.json were touched at all by an external PR. Happy to restructure if you'd prefer the dependency wired differently.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed July 9, 2026, 7:31 PM ET / 23:31 UTC.

Summary
this PR adds a default-enabled bundled dashboard plugin with workspace storage, gateway RPCs, CLI commands, agent tools, binding resolution, package metadata, a lockfile importer entry, and tests.

PR surface: Source +3300, Tests +1400, Config +27, Other +13. Total +4740 across 19 files.

Reproducibility: no. current-main bug reproduction applies because this is a new feature; source inspection of the PR head reproduces the storage, undo-version, and caller-controlled actor blockers.

Review metrics: 3 noteworthy metrics.

  • Persistent state surfaces: 3 file-backed surfaces added. The PR adds workspace.json, an undo JSON ring, and custom-widget scaffold files under stateDir/dashboard, which matters because OpenClaw runtime/plugin state is expected to live in SQLite unless approved as an artifact.
  • Default-on control plane: 1 startup plugin, 14 gateway RPCs, 14 agent tools. Existing installs would receive a new mutating dashboard control plane on startup, so product and security owners should review the upgrade boundary.
  • Dependency graph surface: 1 plugin package dependency, 1 lockfile importer entry. The external PR changes dependency-related files and still needs exact-head maintainer/security authorization even though typebox 1.3.3 is already resolved elsewhere.

Stored data model
Persistent data-model change detected: database schema: extensions/dashboard/src/default-workspace.ts, database schema: extensions/dashboard/src/schema.ts, serialized state: extensions/dashboard/src/cli.ts, serialized state: extensions/dashboard/src/data-read.test.ts, serialized state: extensions/dashboard/src/store.test.ts, serialized state: extensions/dashboard/src/store.ts, and 2 more. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #101093
Summary: This PR is the backend foundation candidate for the modular-dashboard tracking issue; #101097 and #101098 are stack siblings, not replacements, and #101136 tracks broader later-wave work.

Members:

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

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🧂 unranked krab
Result: blocked by patch quality or review findings.

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

Rank-up moves:

  • Move dashboard workspace, undo, and scaffold state to the canonical SQLite/plugin-state model or get explicit maintainer approval for a named artifact exception.
  • [P2] Make undo publish a fresh monotonic workspaceVersion and add focused regression coverage for clients that gate on version changes.
  • Remove caller-supplied gateway actors and derive provenance from trusted request/tool context.

Risk before merge

  • [P1] Merging as-is would add OpenClaw-owned dashboard workspace, undo, and scaffold state as JSON sidecars instead of canonical SQLite/plugin-state storage or an approved named artifact exception.
  • [P1] Because the plugin is default-enabled and startup-activated, existing installs would receive a new mutating dashboard control plane on upgrade.
  • [P1] Gateway writes still trust caller-supplied actor values, so an operator.write client can forge dashboard provenance and approval metadata.
  • [P1] The external PR changes package metadata and the lockfile importer; dependency-graph authorization remains a maintainer/security gate for the exact head SHA.

Maintainer options:

  1. Fix Architecture And Security Before Merge (recommended)
    Move workspace, undo, and scaffold state to SQLite or an approved artifact, keep undo versions monotonic, derive actors from trusted context, and rerun dependency/security approval on the exact head.
  2. Accept The Default-On Boundary Explicitly
    A product/security owner can approve the default-on control plane and any named artifact exception after the remaining correctness fixes are addressed.
  3. Pause The Bundled Foundation
    If the bundled dashboard boundary is not current direction, pause or close this PR and keep the work in the roadmap or external plugin path.

Next step before merge

  • [P2] The PR has concrete repair findings plus a default-on bundled plugin product/security decision that should be resolved by maintainers before any repair or merge path.

Maintainer decision needed

  • Question: Should OpenClaw accept a default-enabled bundled dashboard control-plane plugin after the storage, undo, and actor-provenance blockers are fixed, or should this remain opt-in/external for now?
  • Rationale: The PR adds an always-on product surface with mutating gateway RPCs/tools, persistent state, approval/provenance semantics, and dependency graph changes, so automation cannot choose the product and security boundary safely.
  • Likely owner: steipete — He owns the closest recent bundled plugin plus Control UI tab precedent and appears best positioned to decide whether this dashboard belongs in the same product lane.
  • Options:
    • Harden And Keep Bundled (recommended): Keep the PR open and require SQLite-backed or approved artifact state, monotonic undo, trusted actor derivation, exact-head dependency authorization, and explicit security/product approval before merge.
    • Make It Opt-In Or External: Require the dashboard foundation to ship off by default or outside the bundled core plugin set until the default-on boundary is sponsored.
    • Pause The Dashboard Stack: Pause or close this stack if maintainers do not want a bundled dashboard product surface in the current roadmap window.

Security
Needs attention: The diff introduces a default-on mutating dashboard control plane with approval/provenance state and dependency graph changes, and gateway provenance remains caller-controlled.

Review findings

  • [P1] Store dashboard state in SQLite — extensions/dashboard/src/store.ts:70-73
  • [P2] Keep undo workspace versions monotonic — extensions/dashboard/src/store.ts:141-146
  • [P2] Derive gateway actors from trusted context — extensions/dashboard/src/gateway.ts:83-89
Review details

Best possible solution:

Land only after dashboard state is SQLite-backed or explicitly approved as a named artifact, undo publishes monotonic versions, gateway actor provenance comes from trusted context, and maintainers approve the default-on product/security boundary.

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

No current-main bug reproduction applies because this is a new feature; source inspection of the PR head reproduces the storage, undo-version, and caller-controlled actor blockers.

Is this the best way to solve the issue?

No. A single validated dashboard control plane is plausible, but this implementation needs canonical storage, monotonic versioning, trusted provenance, dependency/security authorization, and maintainer-approved default behavior before it is the best merge shape.

Full review comments:

  • [P1] Store dashboard state in SQLite — extensions/dashboard/src/store.ts:70-73
    This prior blocker remains on the latest head. The store persists workspace.json and undo snapshots under stateDir/dashboard, and the scaffold path adds more files there; root policy requires OpenClaw-owned runtime state and plugin scratch data to use SQLite/shared plugin state unless it is an approved named artifact.
    Confidence: 0.93
  • [P2] Keep undo workspace versions monotonic — extensions/dashboard/src/store.ts:141-146
    This prior blocker remains on the latest head. mutate() increments workspaceVersion, but undo() writes the old snapshot unchanged, so a rollback can publish a lower version and clients that gate refetches on increasing versions may ignore the change.
    Confidence: 0.9
  • [P2] Derive gateway actors from trusted context — extensions/dashboard/src/gateway.ts:83-89
    This prior blocker remains on the latest head. Tool calls derive agent:<id> from tool context, but gateway RPCs still accept actor from params and stamp/broadcast it, letting any operator.write client claim another user, agent:*, or system in dashboard provenance and approval metadata.
    Confidence: 0.91

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P2: This is a substantial new bundled feature with concrete merge blockers, but it is not an urgent regression or current user outage.
  • merge-risk: 🚨 compatibility: The PR adds a default-enabled startup plugin plus persistent dashboard state and package/lockfile changes that affect upgrades and existing installs.
  • merge-risk: 🚨 security-boundary: The PR adds mutating gateway/tool surfaces and approval/provenance state while gateway actor provenance remains caller-controlled.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦞 diamond lobster and patch quality is 🧂 unranked krab.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (screenshot): The PR body screenshot directly shows the after-change control plane composing a dashboard tab; linked videos were unavailable for preprocessing because ffprobe is missing.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body screenshot directly shows the after-change control plane composing a dashboard tab; linked videos were unavailable for preprocessing because ffprobe is missing.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR body screenshot directly shows the after-change control plane composing a dashboard tab; linked videos were unavailable for preprocessing because ffprobe is missing.
Evidence reviewed

PR surface:

Source +3300, Tests +1400, Config +27, Other +13. Total +4740 across 19 files.

View PR surface stats
Area Files Added Removed Net
Source 10 3300 0 +3300
Tests 7 1400 0 +1400
Docs 0 0 0 0
Config 1 27 0 +27
Generated 0 0 0 0
Other 1 13 0 +13
Total 19 4740 0 +4740

Security concerns:

  • [medium] Caller-controlled actor provenance — extensions/dashboard/src/gateway.ts:83
    Gateway write methods accept actor from request params and use it for createdBy, approvedBy, and broadcasts, so an authorized writer can forge dashboard provenance or approval metadata.
    Confidence: 0.9
  • [medium] Default-on mutating control plane — extensions/dashboard/openclaw.plugin.json:5
    The plugin is enabled by default on startup and registers new dashboard gateway/tool write surfaces, expanding the runtime security boundary for upgraded installs.
    Confidence: 0.84
  • [low] Dependency graph authorization still required — extensions/dashboard/package.json:7
    The PR changes plugin package metadata and the lockfile importer; the live dependency guard still blocks until a maintainer/security owner authorizes the exact head SHA.
    Confidence: 0.86

Acceptance criteria:

  • [P1] After repairs, run the dashboard plugin test suite in the approved remote/Testbox path for the exact PR head.
  • [P1] Verify an upgrade-style run where an existing install receives or opts into the dashboard plugin without stale JSON sidecars.
  • [P1] Review and approve dependency graph changes for the final exact head SHA.

What I checked:

  • PR surface: Live PR metadata reports +4740/-0 across 19 files, all under extensions/dashboard plus pnpm-lock.yaml; the branch head is 500fadef347bd0281a2c590a826d8bdfb5e16e9f. (500fadef347b)
  • Current main lacks this plugin: The PR diff adds extensions/dashboard/** from scratch and modifies only the lockfile outside that plugin, so current main has not already implemented this dashboard backend. (500fadef347b)
  • Repository storage policy: Root policy requires OpenClaw-owned runtime state, caches, and plugin scratch data to avoid JSON/sidecar files and use SQLite-backed state unless a named product artifact exception applies. (AGENTS.md:77, 3dd5c1880f46)
  • Dashboard store writes JSON sidecars: DashboardStore persists <stateDir>/dashboard/workspace.json and an undo directory under <stateDir>/dashboard/undo, which are OpenClaw-owned runtime state surfaces rather than approved artifacts. (extensions/dashboard/src/store.ts:70, 500fadef347b)
  • Undo can regress workspaceVersion: mutate() increments workspaceVersion, but undo() validates and writes the old snapshot unchanged, so a rollback can publish a lower version than the current document. (extensions/dashboard/src/store.ts:141, 500fadef347b)
  • Gateway actor is caller-controlled: Gateway write handlers accept an actor parameter and use readOptionalActor() for createdBy, approvedBy, and broadcast provenance. (extensions/dashboard/src/gateway.ts:83, 500fadef347b)

Likely related people:

  • steipete: Authored and merged the Logbook bundled plugin with a Control UI tab seam and has recent SQLite/plugin-state work that is the closest accepted precedent for dashboard-as-plugin storage and UI integration. (role: recent adjacent feature owner; confidence: high; commits: c730d8f1f1bb, 0d981095d430, 8d6a6e9d0325; files: src/gateway/control-ui-plugin-tabs.ts, extensions/logbook/src/store.ts, src/plugin-state/plugin-state-store.ts)
  • vincentkoc: Recent history on src/plugin-state/plugin-state-store.ts and extensions/workboard/src/sqlite-store.ts ties this person to adjacent plugin-state and Workboard persistence changes. (role: recent area contributor; confidence: medium; commits: 8fc88be21643, 00160ea6ee22, ac8a3f367c32; files: src/plugin-state/plugin-state-store.ts, extensions/workboard/src/sqlite-store.ts, extensions/workboard/src/store.ts)
  • BunsDev: Recent Workboard status-persistence work touched the same durable plugin-state class of behavior and validation expectations, though the dashboard ownership trail is less direct. (role: adjacent storage contributor; confidence: low; commits: e07dbb27d91f; files: extensions/workboard/src/sqlite-store.ts, extensions/workboard/src/store.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 (5 earlier review cycles)
  • reviewed 2026-07-06T19:23:08.299Z sha d3736ab :: found issues before merge. :: [P1] Store dashboard state in SQLite | [P2] Keep undo workspace versions monotonic | [P2] Derive the actor instead of accepting it from RPC params
  • reviewed 2026-07-08T04:32:50.477Z sha d3736ab :: found issues before merge. :: [P1] Store dashboard state in SQLite | [P2] Keep undo workspace versions monotonic | [P2] Derive the actor instead of accepting it from RPC params
  • reviewed 2026-07-08T19:07:48.812Z sha ea752b6 :: found issues before merge. :: [P1] Store dashboard state in SQLite | [P2] Keep undo workspace versions monotonic | [P2] Derive gateway actor from request context
  • reviewed 2026-07-08T19:17:27.935Z sha ea752b6 :: found issues before merge. :: [P1] Store dashboard state in SQLite | [P2] Keep undo workspace versions monotonic | [P2] Derive gateway actors from trusted context
  • reviewed 2026-07-08T21:29:17.692Z sha ea752b6 :: found issues before merge. :: [P1] Store dashboard state in SQLite | [P2] Keep undo workspace versions monotonic | [P2] Derive gateway actors from trusted context

A whole-document dashboard.workspace.replace accepted any widgetsRegistry entry
verbatim, so a caller could submit a doc with a custom widget already marked
status:"approved" and skip the operator approval gate entirely (approve is meant
to be the sole transition to "approved").

Add DashboardStore.replaceSanitized, which reconciles the incoming registry
against the CURRENT document inside the write lock (no TOCTOU): any entry that
arrives "approved" without already being "approved" is downgraded to "pending"
and its approval provenance dropped. The workspace.replace gateway method now
uses it; replace stays a trusted primitive for seeding/restore/undo.

Found during the downstream Boardstate extraction's adversarial security audit
(SPEC invariant I3). +3 regression tests.

Formatting proof: oxfmt --write + oxlint clean (node_modules unavailable in this
worktree; committed --no-verify after separate formatting proof).
@100yenadmin

Copy link
Copy Markdown
Contributor Author

Added a security fix to this PR (commit above), from the downstream Boardstate extraction's adversarial audit (SPEC invariant I3): dashboard.workspace.replace accepted any widgetsRegistry entry verbatim, so a caller could submit a doc with a custom widget already marked status:"approved" and skip the operator approval gate. Fixed with DashboardStore.replaceSanitized (reconciles incoming approval against the current doc inside the write lock; replace stays a trusted primitive for seed/restore/undo). +3 regression tests.

Integration note: store.ts is byte-identical on #101098 and #101900, which carry their own copies. This approval-reconcile is a store-level (base) fix and lands canonically here; those branches should pick it up on rebase onto a merged main, or the fix should be applied at integration so none of them ships the hole depending on merge order.

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 backend/control-plane scope is now on main under the bundled Workspaces plugin. The landed version preserves the original direction and contributor credit, while using SQLite for workspace state, remaining disabled by default, and incorporating the subsequent contract and security hardening.

Closing this stacked foundation 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

dependencies-changed PR changes dependency-related files merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants