Live-data contract
Status: Shipped Generated from app @ 1b3d5a69d73473db077dfb44f4390b42cded6bd4 Regenerate: pnpm gen:reference
Before an agent writes a live page, it calls the get_live_data_contract MCP tool. The contract below is what that tool returns, section by section: how a page acts through the bridge verbs, how it uses the dashboard kit, and how it reads routine results. The verb list itself is on Page SDK.
A page never fetches external services itself and never holds a credential. External data is collected by an agent routine, using the agent’s own tools, and the page reads the routine’s completed result.
What it’s for
| Topic | Contract |
|---|---|
role | Author local HTML dashboards using scoped local queries, the agents read model (sdk.agents_and_inbox), routines, agent runs, worktrees and machines (sdk.routines_runs_and_machines), actions and completed routine snapshots. Agents own external credentials and requests. |
Acting
| Topic | Contract |
|---|---|
role | Beyond reading data, any HTML doc in your spaces can act — mutate CodeSense state through the bridge verbs (flip a story's status, route a file into context, draft a doc, open a workspace, run a skill, chat to a running agent). Every allowlisted verb runs when called: there is nothing to declare and no prompt. |
call | await window.codesense.act(verb, args) → Promise. `act` is a thin convenience wrapper over the same invoke RPC. Resolves with the verb's result; rejects with a typed-prefix Error when the call fails. |
tiers.metadata | Silent frontmatter/file and inbox-state writes (setStatus/setField/routeToContext/draft, markRead/setHumanStatus). A runaway guard caps a burst of write calls (a batch `setStatus` is one call); past it the call rejects `denied: write budget` until the burst window passes. |
tiers.spawn | Opens a workspace / drives an agent (openWorkspace/runPrompt/runSkill/openTab), or opens a file in the viewer (openFile) — bounded by a concurrency cap (`busy` when saturated). Opens that take no cap slot (openFile, openTab, a headed dir/epic openWorkspace) are bounded by a runaway guard instead: past a burst the call rejects `denied: spawn budget` until the window passes. |
tiers.send | Injects a chat turn into an already-running agent (sendToSession/sendToWorkspaceTab, and runPrompt), or sends an inbox message (postMessage, sendAsHuman, replyAsHuman) — rate-limited (anti-flood; `denied: send budget`). |
tiers.read | Reads a file / lists a dir confined to the doc's own directory subtree (readWorkspace), or observes a workspace — audited (read:true), never budgeted. |
tiers.manage | Closes a session the board opened (closeWorkspace) — audited, but not budgeted by the metadata write guard and not rate-limited like a chat send, so an autonomous cleanup pass can close many finished sessions in a row. |
errors | A failed call rejects the act() Promise with a stable typed prefix — branch on `err.message.startsWith(...)`: `denied: write budget` (metadata runaway guard) · `denied: route not applied (…)` · `denied: send budget` (chat rate) · `denied: prefs budget` (setPrefs burst) · `denied: spawn budget` (cap-free open burst) · `busy` (concurrency cap saturated) · `not-allowed:` (runSkill: a prompt-splitting skill name / control-char arg) · `unavailable:` (backend / workspace context / agent CLI down) · `traversal:` / `unsupported-type:` / `too-large:` / `binary:` / `not-found:` (readWorkspace path/content rejects) · `refused:` / `not-found:` (openFile outside a known team content root, or a missing file) · `invalid:` (setField: a bad key name or value) · a verbatim CodeSense reject for an out-of-bounds (non-managed) path. Same typed-prefix convention as `analyze`. |
enforcement | The runaway guards, the concurrency cap and managed-path confinement run in the CodeSense process, never in your doc's JS. Your act() call is convenience; those guards live behind it. |
safe_output | Treat any value an action returns — and all upstream/model data — as unsafe input: render with textContent / createElement, never innerHTML / outerHTML / document.write. |
authoring.role | Everything a dashboard author needs to build an acting dashboard without reading the CodeSense source — the doc scaffold, the epic-addressing primitive, the working-directory rule, and the pre-ship check. |
authoring.scaffold | Add `<meta name="cs:type" content="dashboard">` to <head> so the doc is enumerable as a dashboard. Nothing else needs declaring: call any verb directly. |
authoring.get_doc_dir.call | await window.codesense.invoke("getDocDir") |
authoring.get_doc_dir.returns | the absolute directory of this dashboard doc; null when no dashboard is mounted. |
authoring.get_doc_dir.use | The anchor for building an absolute path to a sibling doc (e.g. a story) for runSkill — available without an open epic workspace. `abs = getDocDir() + '/' + name`. |
authoring.get_epic_handle.call | await window.codesense.invoke("getEpicHandle") |
authoring.get_epic_handle.returns | the epic's absolute doc path (epicDocPath) when this dashboard is hosted inside an open epic workspace; otherwise null. |
authoring.get_epic_handle.use | How a dashboard addresses the epic's already-running agent (the `target` for sendToSession). When null, the doc is not inside an open epic workspace — hide the epic-chat controls, never render a dead button. (For sibling absolute paths, prefer getDocDir, which needs no open epic.) |
authoring.cwd_rule | runSkill and sendToSession launch the user's agent CLI in a different working directory than the doc — pass absolute paths, never doc-relative ones. Build them from getDocDir() (the board's own dir, available without an open epic); getEpicHandle() also yields an absolute anchor but only inside an open epic. readWorkspace is the exception: it is confined to the doc's own directory subtree, so it takes './'-relative paths. |
authoring.non_blocking | Never await a full readWorkspace sweep before first paint. Paint the shell + skeleton rows synchronously, then hydrate rows off the read promises (Promise.all) so the UI is interactive immediately. |
authoring.validate | Run validate_live_data_doc on your HTML before shipping: it flags a setStatus call addressed by storyId instead of path (blocking), a sendToSession target not resolved via getEpicHandle (advisory), and unsafe innerHTML (advisory). |
Dashboard kit
| Topic | Contract |
|---|---|
role | Every live page (a dashboard file, a right-pane pane, an html inbox message) gets the dashboard kit with no tag to add: a stylesheet of tokens and opt-in `cs-` classes, and window.codesense.kit. A page that uses neither looks as it did. Use them to look like the built-in panes (Agents tree, Pipeline, Inbox, Routines). |
tokens | CSS custom properties beside the theme colours: --cs-space-1..6 (4, 8, 12, 16, 24, 32px), --cs-radius-sm / --cs-radius / --cs-radius-lg / --cs-radius-full, --cs-text-xs / -sm / -base / -lg / -xl (11–18px), --cs-leading, --cs-font-mono, --cs-hover and --cs-inset (derived surfaces), --cs-shadow; the agent-state palette --cs-state-running, --cs-state-idle, --cs-state-needs-you, --cs-state-exited, --cs-state-offline, --cs-state-unknown; signal_progress phases --cs-phase-planning | -dev | -review | -pr | -blocked | -done; default story statuses --cs-status-planning | -todo | -in-progress | -blocked | -review | -qa | -done (a team's taxonomy() colours win). |
classes | cs-app (on <body>: theme background, text, font, spacing), cs-stack / cs-row / cs-grid / cs-spacer / cs-toolbar (layout), cs-card + cs-card-header + cs-card-body (is-interactive, is-selected), cs-list, cs-table (cs-num cells), cs-badge (is-accent | is-success | is-warning | is-danger, or style --cs-badge-color), cs-count, cs-dot (data-state = a sessions() state), cs-btn (is-primary | is-ghost | is-danger | is-sm | is-icon), cs-input, cs-tabs + cs-tab (aria-selected="true"), cs-empty, cs-notice (is-warning | is-danger), cs-avatar, cs-progress (a <span> child with a width), cs-kv (a <dl>), cs-pre, cs-drawer + cs-drawer-header + cs-drawer-body (a side panel; toggle `hidden`), cs-segmented (a group of buttons, aria-pressed="true" on the chosen one), cs-btn[aria-pressed] (a toggle), cs-live (a live/stale indicator: is-stale, is-loading), cs-skeleton*, cs-h1 / cs-h2 / cs-h3, cs-muted, cs-small, cs-xs, cs-mono, cs-truncate, and cs-dag* (kit.dagView / kit.dagSvg; nodes get is-selected, is-focus, is-upstream, is-downstream, is-dim). Inside cs-app, [hidden] always hides. |
helpers.h | kit.h(tag, {class, text, style, dataset, onClick, ...attrs}, ...children) → an element; strings become text, never HTML. kit.s(...) is the same for svg. |
helpers.icon | kit.icon(name, {size?, strokeWidth?, class?, title?}) → an inline <svg> Lucide icon (the app's glyphs, isc licence). kit.iconNames() lists the bundled set (agents, stories, routines, machines, chrome: bot, network, workflow, git-branch, git-pull-request, inbox, calendar-clock, server, terminal, ...); an unknown name draws circle-help. |
helpers.status | kit.statusDot(state) and kit.statusBadge(state) for a sessions() state (running | idle | needs-you | exited | offline | unknown), with the app's colours and labels; kit.phaseBadge(phase) for progress.phase; kit.storyBadge(status, taxonomyState?) for a story. kit.agentStates / kit.phases / kit.storyStatuses are the palettes ({label, color, icon}). |
helpers.avatar | kit.avatar({face?, name, size?}) → a round avatar: a built-in face ref (a sessions() row's avatar, e.g. 'builtin-funny:coffee-bot') draws that face, anything else the name's initials. |
helpers.layout_dag | kit.layoutDag(nodes, edges, {direction: 'tb'|'lr', nodeWidth: 160, nodeHeight: 44, nodeSep: 24, rankSep: 56, group?: (node) => key, maxWidth?}) → {width, height, nodes: [{id, x, y (top-left), cx, cy, width, height, layer, order, data, group?}], edges: [{from, to, points: [{x, y}], reversed, cross?, data}], groups?: [{key, x, y, width, height, count}], reversed, skipped, crossings}. nodes are ids or {id, width?, height?, ...}; edges {from, to} with `from` drawn first (launcher → launched, dependency → dependent). Layered by longest path, ordered to cut crossings; a cycle is drawn by reversing a back edge (listed in `reversed`); self-loops and unknown ids are skipped. With `group` (an epic, a machine) each group is laid out in its own framed cluster and edges between clusters are marked `cross`. |
helpers.dag_view | const view = kit.dagView(container, {renderNode: (node) => element, renderGroup?, group?, onSelect?(id), onActivate?(id) (double-click), nodeVersion?(data), ...layoutDag options}); view.update(nodes, edges) on every read. A pannable, zoomable graph (drag or wheel to pan, pinch or ⌘-wheel to zoom, buttons to zoom and fit), packed to the view's shape. A re-read with the same shape re-renders only the nodes whose data changed and keeps pan and zoom; hovering or selecting a node lights everything upstream and downstream. view.select(id), view.center(id), view.fit(), view.layout(). Use this for an agent launch tree or a story dependency graph. |
helpers.dag_svg | kit.dagSvg(layout, (node) => element, {edgeClass?}) → a static <div class="cs-dag">: an svg of curved arrows with each node's element (e.g. a div.cs-dag-card) placed over it. kit.edgePath(points, direction) is the path data alone. |
helpers.keyed | kit.keyed(container, items, {key, render, version?}) renders a list by key, reusing an item's element while its version (default JSON) is unchanged, so a live re-read patches rows instead of redrawing them. |
helpers.skeleton | kit.skeleton({rows, avatar?}) → shimmering placeholder rows to show until the first read lands. |
helpers.call | kit.call(verb, args) → the named helper (codesense[verb]) when this app has it, else invoke(verb, args); a call this app does not know rejects with err.unavailable === true, so show a notice instead of failing. kit.has(verb) checks first. |
helpers.live | const stop = kit.live(render, {events: ['sessions-changed', 'agents-changed'], pollMs: 30000}) runs render now, again (coalesced) on each named host event (codesense:<event>), and every pollMs while on screen. kit.on(event, cb) listens for one event. |
helpers.misc | kit.timeAgo(msOrIso), kit.empty(text, icon?) (say what to do next, e.g. how to start one), kit.notice(text, 'info'|'warning'|'danger', {label, onClick}?) (e.g. an error with Retry). |
states | Show every state: kit.skeleton() while loading, kit.empty() that says how to get started when there is nothing, kit.notice(…, 'danger', {label: 'Retry', onClick}) on an error, and a short notice when kit.call rejects with err.unavailable (an app too old for the call). Below ~560px (a narrow right pane) prefer a list or tree to a graph. |
references | Working rebuilds of built-in panes in the CodeSense docs context repo (getting-started team), content/examples/dashboards/: agent-dag.html (the Agents tree as a launch DAG), pipeline-graph.html (story dependency graph), routines-console.html (routines, run history, run detail). Copy one to start. |
Routine results
| Topic | Contract |
|---|---|
refresh | Read routineResult/routineStatus on page load. Only an explicit user action may request routineRefresh; page load must not launch an agent. |
Snapshots
| Topic | Contract |
|---|---|
snapshot | codesense.snapshot captures a frozen record; never execute saved snapshot state. |
Analyze
| Topic | Contract |
|---|---|
analyze | invoke('analyze') uses the configured agent CLI; handle unavailable, timeout and busy errors, and render model output as text. |
Dashboards as inbox messages
| Topic | Contract |
|---|---|
inbox_messages | An agent can send the human a dashboard as a message: send_message with {to:"human", contentType:"html", text:<the HTML>}. It renders in the Inbox exactly like a dashboard file — scripts, window.codesense (reads, act() through the same action gate) and pushed events — sized to its content, with Open in tab. Always pass {summary}: one plain-text line (≤280 chars) the Inbox and lists show for it, since a line derived from the markup reads badly. Team-scoped calls act in the message's team. Example text: `<p id=o>…</p><script>codesense.sessions().then(s=>{o.textContent=s.length+' sessions running'})</script>`. |
Retired services
| Topic | Contract |
|---|---|
retired_services | Built-in provider connections, connector polling and /api/v1/proxy are unavailable. Publish external data through a routine and read its result. |
See also
| Topic | Contract |
|---|---|
see_also | validate_live_data_doc, routine_result, routine_publish_result |
Linter rules
validate_live_data_doc checks a page before it’s saved. Blocking findings fail the check; advisory ones are warnings.
| Code | Severity | Finding | Fix |
|---|---|---|---|
HARDCODED_TOKEN | blocking | The doc appears to contain a hardcoded credential (a known provider token prefix). | Remove it. Credentials belong to the configured agent, never the dashboard. |
EMBEDDED_AUTH_HEADER | blocking | The doc sets an Authorization header to a literal credential. | Use routine snapshots; keep external credentials in the configured agent. |
RETIRED_SERVICE_ACCESS | blocking | This dashboard uses retired built-in service access. | Use an agent-owned routine to publish data and read it with routineResult; refresh only on an explicit user action. |
UNSAFE_INNERHTML | advisory | Upstream provider data or analyze model output may be rendered via innerHTML/outerHTML/document.write. | Treat all upstream and model output as unsafe input — render with textContent / createElement. |
ANALYZE_NO_DEGRADE | advisory | An analyze() call has no error handling — no catch or startsWith branch for the rejection. | Wrap the call in try/catch (or a two-arg .then) and branch on err.message.startsWith('unavailable'|'timeout'|'busy') — the CLI may be absent, slow, or busy. |
ANALYZE_EXPECTS_STREAMING | advisory | The doc appears to expect a token stream around analyze(). analyze() returns a single completed string — it does not stream. | Render an 'Analyzing…' placeholder, then swap in the completed result via textContent. Drop any streaming/token-by-token handling. |
SNAPSHOT_STATE_EXECUTED | advisory | Embedded snapshot state appears to be executed (eval / new Function) instead of parsed as inert data. | Re-hydrate snapshot state with JSON.parse(el.textContent) and render via safe DOM — never eval / new Function / innerHTML. |
SETSTATUS_STORYID_ARG | blocking | setStatus is called with a `storyId` and no `path` — setStatus addresses the story doc by its content path, so this call always rejects. | Pass the doc's path: act('setStatus', {path: doc.path, status}). queryContext() rows carry `path`. |
SENDTOSESSION_HARDCODED_TARGET | advisory | sendToSession is called but getEpicHandle() is never used — the target is likely a hard-coded path that won't resolve on another machine or epic. | Resolve the target at runtime: sendToSession(getEpicHandle(), …). Hide the control when getEpicHandle() is null. |
Starter page
The contract ships this page as a starting point. It follows every pattern above.
<!doctype html>
<html><head><meta charset="utf-8"><meta name="cs:type" content="dashboard">
<title>Routine snapshot</title></head><body>
<h1>Routine snapshot</h1><p id="status">Loading cached result…</p>
<button id="refresh" type="button">Refresh routine</button><pre id="result"></pre>
<script>
// Replace with your routine key.
const key = "team:example";
const status = document.getElementById("status");
const result = document.getElementById("result");
async function readSnapshot() {
if (!window.codesense) { status.textContent = "Open this dashboard in CodeSense to read routine results."; return; }
try {
const value = await window.codesense.act("routineResult", {key});
result.textContent = JSON.stringify(value, null, 2);
status.textContent = "Cached routine result";
} catch (error) { status.textContent = String(error); }
}
document.getElementById("refresh").addEventListener("click", async () => {
if (!window.codesense) return;
try { await window.codesense.act("routineRefresh", {key}); status.textContent = "Refresh requested; reopen or read the result after completion."; }
catch (error) { status.textContent = String(error); }
});
readSnapshot(); // Read only: page load never launches a routine.
</script></body></html> Generated from app @ 1b3d5a69d73473db077dfb44f4390b42cded6bd4 Regenerate: pnpm gen:reference
How we know this is accurate 2 sources
Checked against the app at 1b3d5a69d73473db077dfb44f4390b42cded6bd4.
Code
-
src-tauri/src/mcp/live_data_authoring.rs -
src-tauri/src/mcp/live-data-starter.html