Ctrl K
ComunidadGuías…

docs/agent/console-workspaces.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Console Workspaces — the fact-driven nav/page config (M16b)

> How an agent (via MCP `store_fact` / `PUT /v1/facts`) or the console Studio
> produces the SAME workspace/page configuration. The console reads these facts
> at boot and renders identically regardless of who wrote them — "one artifact,
> two editors". Enforcing code: `crates/corecruxd/console/v2/render.js` (`cws*`
> helpers) + `shell.html` (nav runtime). Proving test: `smoke.cjs` Check 52.

Operator-named **Workspaces**: a signed **pack** installs a **workspace** (of
**pages**) built in the **Studio**. Configurable Workspaces generalise the old
binary Command | Explorer surface toggle into N workspaces, where the two
built-ins stay auto-generated from the page registry and user workspaces are
config facts that overlay them.

## Entities

Two fact entities, each with key `def`, value = **canonical key-sorted JSON**
(see Canonicalization). Tenant = `default` (the console tenant).

| Entity | Purpose |
|---|---|
| `console:workspace:<uid>` | A workspace: an ordered set of destinations (nav groups), each holding page uids. |
| `console:page:<uid>` | A page instance: a typed page (any registry page id) with title/sub/dest/config. |

`<uid>` is a stable slug. User workspace uids are conventionally `ws-<slug>`;
user page uids `ws-<slug>-<page-slug>`. The uids `command` and `explore` are
**reserved** for the two built-ins (writing a `console:workspace:command` fact
*forks* that built-in — see Take control).

## Schemas (schema_version 1)

### Workspace `def`

```json
{
  "schema_version": 1,
  "uid": "ws-ops",
  "name": "My ops board",
  "icon": "meters",
  "order": 100,
  "source": "user",
  "dests": [
    { "id": "ops", "label": "Ops", "icon": "meters", "pages": ["ws-ops-execplans", "ws-ops-facts"] }
  ]
}
```

- `source`: `"user"` | `"builtin-fork"` (a forked built-in also carries
  `"forked_from": "command"|"explore"`).
- `dests[].pages`: an ordered list of page uids. A page uid may reference a
  `console:page:<uid>` fact OR, in a forked built-in, a bare registry page id
  used directly as a type (e.g. `"cx-facts"`).
- `order`: switcher/rail ordering (ascending).

### Page `def`

```json
{
  "schema_version": 1,
  "uid": "ws-ops-execplans",
  "type": "cx-work",
  "title": "ExecPlans",
  "sub": "read-time projection over the work board",
  "dest": "ops",
  "config": { "query": { "source": "all" } },
  "source": "user"
}
```

- `type`: **any page type that exists in the console** — every registry page id
  (`cx-overview`, `cx-work`, `cx-facts`, `cx-cost`, …) plus the
  destination-IS-the-page surfaces (`canvas/board`, `canvas/graph`,
  `canvas/tree`, `explorer`, `sitemap`, `rings`). The page renders through the
  SAME builder as the built-in console, so "whatever exists now is generatable".
- `config.query`: for endpoint-driven types with declared options (currently
  `cx-work` → `source`, `cx-memory` → `top_k`, `cx-review` → `limit`), these
  merge into the load endpoint's query string. Other types ignore config and
  render their registry default. Unknown config keys are preserved (tolerant
  reader) but not interpreted.

### Tolerant reader (CONTRACT)

- Unknown keys — top-level and nested — **survive untouched** through a Studio
  round-trip (the reader spreads/merges; it never rebuilds field-by-field). Add
  fields freely; older consoles preserve them.
- A `schema_version` **newer** than the console understands is returned
  verbatim and rendered as an honest "newer configuration" panel — never
  destroyed or reinterpreted. The daemon (not the SPA) owns migrations.

## Canonicalization (byte-stable — one artifact, two editors)

The value string is JSON with **keys sorted deeply**, arrays order-preserved, no
whitespace. The Studio writes exactly this; an MCP writer must produce the same
to get byte-identical storage. Because the reader parses JSON (order-independent)
and renders from the parsed def, ANY valid JSON with the right fields renders
identically — canonicalization only matters for byte-equality / diffs.

Reference (render.js): `cwsCanonical(v)` — recursively sorts object keys, then
`JSON.stringify`. Equivalent to: sort every object's keys ascending, emit
compact JSON.

## Take control (reversible fork) + revert

- Built-in workspaces/pages render from the registry until an operator **forks**
  one in the Studio: the fork copies the generated def into an overlay fact with
  `source: "builtin-fork"`, `forked_from: <builtin-uid>`. A provenance chip
  ("forked from built-in · revert") shows in the Studio.
- **Revert** = write a **tombstone** def through the same gated fact-add path:
  `{ "schema_version": 1, "uid": "<uid>", "reverted": true }`. The reader treats
  a reverted overlay as absent → auto-generation resumes for a built-in, or the
  user workspace/page drops out. (There is no gated fact-DELETE client method;
  the tombstone is a soft-delete matching the daemon's own model, and re-forking
  overwrites it.)

## Writing a workspace via MCP / HTTP (parity proof)

The daemon fact surface is the MCP-equivalent write. Either `PUT /v1/facts` (or
MCP `store_fact`) or the console route `POST /v1/console/facts/add` works. Value
must be the canonical JSON **string**.

```bash
# 1. the workspace
curl -sX POST http://<host>:14800/v1/console/facts/add -H 'content-type: application/json' -d '{
  "entity":"console:workspace:ws-mcp",
  "key":"def",
  "value":"{\"dests\":[{\"icon\":\"work\",\"id\":\"main\",\"label\":\"Main\",\"pages\":[\"ws-mcp-facts\"]}],\"icon\":\"work\",\"name\":\"MCP demo\",\"order\":100,\"schema_version\":1,\"source\":\"user\",\"uid\":\"ws-mcp\"}"
}'
# 2. a page of an existing type (renders the real facts store)
curl -sX POST http://<host>:14800/v1/console/facts/add -H 'content-type: application/json' -d '{
  "entity":"console:page:ws-mcp-facts",
  "key":"def",
  "value":"{\"config\":{},\"dest\":\"main\",\"schema_version\":1,\"source\":\"user\",\"sub\":\"the durable record\",\"title\":\"Facts\",\"type\":\"cx-facts\",\"uid\":\"ws-mcp-facts\"}"
}'
```

Refresh the console → the workspace appears in the switcher and rail, and
`#/w/ws-mcp/ws-mcp-facts` renders the real facts browser. (On an authenticated
daemon these are admin-write routes; the console uses its operator-gated
`consoleFactsAdd` path.)

## Packs (additive)

A Studio pack (`crux.studio.v1`, wrapped in a signed `crux.integration.v1`
manifest via `/v1/studio/pack/{build,verify}`) may carry two **optional** arrays
alongside `board`/`designs`/`settings`:

```json
{ "schema": "crux.studio.v1", "board": {…}, "workspaces": [ <workspace def> … ], "pages": [ <page def> … ] }
```

Older importers ignore the extra arrays (the daemon hashes the whole studio
value as an opaque object, so integrity holds). Import applies workspaces/pages
as a defaults layer with provenance (the pack uid); operator edits overlay them.
Export from Studio › Pages includes the selected workspace + its page facts.

## Grepable symbols (render.js)

`cwsCanonical` · `cwsReadWorkspaceDef` · `cwsReadPageDef` · `cwsBuiltinWorkspaces`
· `cwsEffectiveWorkspaces` · `cwsForkWorkspace` · `cwsTombstone` ·
`cwsStarterTemplates` · `cwsPageTypes` · `cwsPackEmbed` / `cwsPackExtract` ·
`cwsLoadOverlays` / `cwsBuildModel` · `renderWorkspacePage` ·
`renderWorkspaceStudio` · `renderIntegrationsStudio`. Nav runtime in `shell.html`:
`loadWorkspaceModel` · `activateWorkspace` · `renderWorkspace` ·
`buildWorkspaceRail` · `window.CRUX_WS_MODEL` / `window.CRUX_WS_RELOAD`.