In the KDCube ecosystem, apps expose their domains as realms — the objects and actions an app owns. The pinboard (the canvas) is where objects from many realms come to sit together. But it does something subtler than "store things."

A pin is not a copy of an object. It's a proxy: a small card that holds a canonical handle to something that lives elsewhere, plus where it came from and what you wrote about it. The board owns the card; the realm still owns the object. So the pinboard is a board of proxies — a context hub where refs to tasks, memories, files, metrics, notes, and provider objects gather, compose, and route back to the apps that own them. A conversation is an object too — pin a chat to reopen it later — and anything a chat produces can be pinned the same way, because these are generic proxies as well. The one rule that keeps it clean: the board never owns what a ref means.

What a pin actually stores

Pin a task, a memory, a file — the card persists only the proxy, never the provider's data:

  • the object_ref — a canonical, opaque URI the realm owns (task:issue:BUG-123, mem:record:mem_456, fi:…/report.pdf, cnv:ut_…).
  • provenance — which surface and namespace it came from.
  • layout — position/size on the board (rect, placement).
  • annotations — your description and comments (these the canvas does own).

A conversation itself (a cnv:/fi: chat artifact) and anything a chat produces — files, drafts, results — pin as proxies exactly like a task or memory: same object_ref model, same resolve/open path.

What it never stores: the realm's schema, bytes, permissions, or lifecycle. Those stay with the provider. Within a single board, dragging the same task:issue:BUG-123 twice gives you the same card back rather than a duplicate. This is per-board identity, not a global cross-board dedup: the same ref can live on many boards at once.

A single pin card, canvas-owned, holds an opaque object_ref handle, provenance, rect/layout, and comments, with an arrow down to the Task realm that owns schema, data, and actions — the object itself lives elsewhere.
The board owns the card; the realm still owns the object.

One board, many realms

Because every pin is just a ref with a known owner, objects from different realms sit side by side — a task next to a memory next to a file next to a metric next to a hand-typed note. The board is the neutral meeting space; each realm stays independent.

ONE BOARD, MANY REALMS — OBJECTS FROM DIFFERENT WORLDS SIT SIDE BY SIDE Pinboard / Canvas the neutral board task task:issue:… memory mem:record:… file fi:…/report.pdf metric usage:metric:… note cnv:provided:… ticket acme:ticket:… Tasks realm Memories realm Files / Metrics storage · usage Canvas notes Provider realm each owns its objects, schema, and what its actions mean.
Each owns its objects, schema, and what its actions mean.

The card shows a cached title and preview (so the board stays readable), but the board treats the ref as opaque. It asks the realm's resolver through the full object_refobject.resolve / capabilities / preview — to fill the display and learn what the object can do. It does not parse the URI. The card's kind is a display/source label only; it never drives behavior. The authoritative path is always the full object_ref through the resolver.

How a pin gets onto the board

Pins arrive by moving a ref, not by copying data. The flow is a clean hand-off where each layer does exactly its job — a source surface offers the object, the scene host knows where it came from and routes it, the canvas stores it as a card without inferring actions, and the resolver decides what the object can do.

Only the canonical ref + provenance travel. The source never ships its schema; the scene never interprets the object; the canvas never decides actions; only the owning provider does. (Agents can pin too, via named_services.upsert_object(namespace="cnv", object_kind="canvas.card", …).)

The ingress protocol

The hand-off has a concrete wire shape. A source widget emits a single canonical message, kdcube.canvas.ingress; the scene host is the only layer that knows the deployment, so it validates the packet, stamps payload.source from its scene config, and routes the drop into the pinboard as kdcube-pinboard-drop-ingress. The pinboard then creates or updates a proxy card, preserving the object_ref untouched, and the provider resolver does the rest later.

THE INGRESS PROTOCOL — ONLY THE REF AND PROVENANCE TRAVEL source widget knows the object / text payload kdcube.canvas.ingress scene host validates · stamps payload.source from scene config routes the drop kdcube-pinboard-drop-ingress pinboard creates / updates the proxy card preserves object_ref later provider resolver resolves preview / open / download / action the widget can't forge provenance — only the scene stamps payload.source.
The widget can't forge provenance — only the scene stamps it.

There are two canonical packet shapes. The first carries a reference to a provider-owned object:

{
  "type": "kdcube.canvas.ingress",
  "payload_type": "object.ref",
  "payload": {
    "object_ref": "mem:record:mem_fc12861ede7c4944ad5df99541185189",
    "mime": "application/json",
    "title": "Excel with openpyxl charts",
    "preview": "User received a crash in Apple Numbers...",
    "presentation": {
      "label": "memory",
      "namespace": "mem",
      "object_kind": "mem:record"
    }
  }
}

The second carries raw content the user dropped (a text selection becomes a canvas-owned note):

{
  "type": "kdcube.canvas.ingress",
  "payload_type": "content.text",
  "payload": {
    "title": "Selection",
    "content": {
      "mime": "text/markdown",
      "text": "Selected text to place on the canvas."
    },
    "presentation": {
      "label": "provided.text",
      "object_kind": "cnv:provided:text"
    }
  }
}

Four wording rules keep the protocol honest:

  • payload_type is a schema selector, not a routing key — it tells the canvas which packet shape to validate, nothing about where the drop goes.
  • presentation is cosmetic and optional — labels, colors, icons; never a behavior input.
  • source is stamped by the scene, not the source widget — only the scene knows the real provenance, so a widget can't forge it.
  • Browser wrappers like kdcube-canvas-ingress-drag-start are transient drag-lifecycle messages — they coordinate the live drag; the ingress packet above is the durable contract.

Showing a proxy without owning its schema

The pinboard has to display things it doesn't understand. KDCube splits that by owner:

  • For people, the card renders from cached metadata + the resolver's object.resolve / preview. The canvas knows "a card with a title and a preview" — not "a task" or "a memory."
  • For agents, a pinned object is materialized on demand. ReAct pulls the ref (react.pull), the owner provider streams the object into the agent's workspace as a local artifact (the canonical object_ref preserved), and the provider's block.produce turns it into bounded, model-visible blocks. An optional block.render hook refines how its blocks appear.
SHOWING A PROXY TO AN AGENT — MATERIALIZED ON DEMAND, RENDERED BY ITS OWNER pinned on the board mem:record:mem_123 react.pull ReAct rehoster Memory provider object.get(stream) bytes streamed in · object_ref kept local artifact fi:…/mem_123.json react.read provider block.produce block.render (optional) refines bounded [MEMORY] blocks meta.object_ref kept model sees the object rendered by its owner, not the canvas the provider owns the representation — ReAct's generic code never branches on mem vs task.
The provider owns the representation — not the canvas.

The point: the provider owns the representation. ReAct's generic code carries identity and never branches on mem vs task. The canvas stays reusable because it never learns any realm's semantics.

From a pin back to the rich app

A proxy is a one-click path home. Click an action on a card and the canvas calls object_action with the full ref; the resolver registry dispatches to the owner, which decides what happens.

FROM A PIN BACK TO THE RICH APP — ONE CLICK, THROUGH THE OWNER click "Open" task:issue:BUG-123 object_action({ action: open }) resolver registry dispatch to owner Task resolver owns the task: grammar · returns ui_event.target_surface scene routes Task widget opens & focuses the issue the canvas never infers from kind — the owner decides what "open" means.
The canvas never infers from kind — the owner decides.

Resolvers offer a small, generic vocabulary — object.resolve, capabilities, preview, open, download — and the provider decides which apply to which ref. A task:issue:… may open; its attachment may download; the canvas never infers that from the URI or the card's kind. Drag a card to its owning widget and the same open runs; drag it to another board and you pin the same ref there. The proxy always knows the way back.

Who works the board

  • People compose context: drag objects from search, chat, memories, and provider widgets onto one board; annotate them; multi-select a batch to attach to a chat turn; search pins across boards. Provenance is always visible — you can see which app and realm each pin came from.
  • Agents read the board as context: react.pull(["cnv:main"]) to see the cards, materialize any pin to inspect it, and write back through named_services.upsert_object — adding cards, comments, or suggestions. Layout stays user-owned; agents suggest, people arrange.

So a board becomes a shared, durable, multi-realm context — usable by a person and an agent at the same time, each through their own surface.

Named boards become portable context

Boards are namedcnv:main, cnv:research, cnv:incident-481 — so you keep as many as you like, each organizing objects from a different world: a research board mixing memories, files, and news; an incident board mixing tasks, metrics, and logs; a personal board of half-finished notes. The pinboard is your filing system across realms — you curate context now and use it later.

Here is the payoff. At any moment you hand a whole board — or just a selection from it — to the chat, and it all becomes usable context in a single move. You don't pre-process anything, and the agent needs no per-realm integration. Because every pin is a proxy, the agent resolves the entire board universally: each object_ref is pulled back through its owning realm with the named-service tools, rendered by that provider's governed block-production policy, and placed into the turn as model-visible context — each object represented the way its owner decided, not the way the board guessed.

NAMED BOARDS BECOME PORTABLE CONTEXT — ORGANIZE ONCE, POUR INTO ONE TURN NAMED BOARDS cnv:research memories · files · news cnv:incident tasks · metrics · logs a selection a few pins from one board "use these" chat / ReAct resolves every pin universally for each pin named-services resolution object_ref → named-service pull → block.produce (governed) one turn of context tasks memories files metrics notes from many apps, each owner-rendered · ready to summarize, compare, draft, or act a named board is portable context — every proxy knows the way home, every realm controls how it appears.
One turn of context: many named boards, many apps, each pin owner-rendered.

So a named board is portable context: organize once across worlds, then pour the whole thing into a conversation and build with it freely — because every proxy knows the way home, and every realm controls how its objects appear. You get the reach of many apps with the discipline of one neutral surface.

The rules that keep it a board, not a database

The whole model holds because the canvas stays disciplined.

  • Must not own provider schemas — a kind is informational; the resolver is authoritative.
  • Must preserve the canonical ref — never a signed URL or transport-only path.
  • Must not infer actions from a ref's URI or card metadata.
  • Must version every change — durable revisions, optimistic concurrency on base_revision.
  • Must not become a data warehouse of every realm's objects — it stores proxies and revisions, not copies.
  • Must expose provenance to the user.
  • Must not parse a provider's URI grammar — refs stay opaque to everyone but their owner.
  • Must always call the resolver for actions — and keep a pin valid even if its resolver isn't loaded yet (actions stay hidden until it is).

Setting it up

Integrating a pinboard into a scene is mostly wiring contracts, not writing canvas logic. A developer can hand the four groups below to an agent as the integration brief.

  • Frontend scene config. Mount a pinboard component with a surfaceRef, targetSurfaces, a route, a runtime, and its command set. Configure a context drop target with dropEffect: "pin" and ingressDelivery: "pinboard.ingress" so dropped refs flow through the canonical ingress packet. Configure objectActions.gateway so open actions reach the provider resolvers, and namespacePresentation so cards pick up each provider's colors and icons.
  • Backend app config. The app must provide the canvas/pinboard widget and expose the cnv named-service provider for board search, schema, and upsert. Register named-service resolvers for every displayable or openable namespace (for example mem and task). Give ReAct its named-service tools with tool_traits — without them, strategy marking and harness validation are wrong. ReAct event-source policies must connect react.pull / react.read to provider object.get and block.produce.
  • Provider requirements. Each provider must hand out canonical object_refs, publish namespace presentation metadata, and implement object.resolve / object.action. To let agents read the object, implement object.get. For a model-facing representation, implement block.produce (and, optionally, block.render).
  • Runtime requirements. If pin indexing is semantic, the embedding/search service must be configured. The Event/Data Bus path must use descriptor-resolved app config, not a blank runtime config. And canvas upsert should fail loudly if required indexing can't complete — otherwise the board and its search index quietly diverge.

What must work

If the integration is correct, this end-to-end path holds:

drop memory/task/file/conversation on pinboard
  -> console shows kdcube.canvas.ingress (or a surface command pin)
  -> backend shows namespace=cnv operation=object.upsert
  -> [canvas.patch] applied
  -> [canvas.pins.index] updated
  -> card opens / resolves through object.action

open a pin
  -> canvas calls object.action(open)
  -> provider returns ui_event.target_surface
  -> scene routes the command to the owning widget

ask ReAct about the canvas
  -> react.pull(["cnv:main"]) for the board
  -> react.pull / react.read provider object refs for deeper inspection
  -> provider block.produce controls the model-visible representation

Bad signs that something in the chain is unwired:

Resolving... forever
canvas_object_resolver_not_registered
401 embedding errors during indexing
object.action result has no ui_event when open should be supported
canvas_revision_conflict on a normal single-user action
duplicate board names

Closing

The pinboard isn't a folder and it isn't a database. It's a board of proxies — a place where references to objects from many realms gather, carry their provenance, compose into reusable context, and route back to the apps that own them. The canvas holds the cards; the realms hold the objects; the resolver decides what each object can do. That separation is what lets one neutral board sit at the center of an entire ecosystem of apps — for people and agents alike.

KDCube Journal · 23.06.2026 · The canvas/pinboard guide, pin integration and operations, cross-surface context drag, named services, and ReAct object materialization all live in the SDK docs.