meta_gateway serves it directly. A persistent, SDK-driven
rpc_gateway also mounts the console routes, but the two binaries are not
interchangeable: SDK hosts drive rpc_gateway over stdio, while
meta_gateway accepts its init handshake and then serves console/admin HTTP.
The console provides real-time visibility into agents, conversations, events,
logs, approvals, and recovery actions. The stock shell is built from shared
@console-core and @console-components packages, while app-specific policy
stays in runtime config and host code.
Accessing the console
Once the runtime is serving HTTP, open the console at:http_base_url in their init result. A Rust library host chooses its own bind
address. http://localhost:8080/console is correct only when your host or
reverse proxy explicitly publishes Meta there.
The HTML, JavaScript, and CSS are compiled into the binary via include_str!, so no build step is required at deployment time. The stock CSS carries its fonts, Inter Tight and IBM Plex Mono (SIL Open Font License 1.1), inline as data: URLs, so the console requests no font from another origin and looks the same on a host without internet access. A Content-Security-Policy in front of the console that restricts fonts must allow font-src data:.
For development with instant hot-reload, see the development workflow, including the current Vite proxy limitations:
Developer custom panels
Products embedding Console can add panels directly to its sidebar. A panel is trusted application code and is independent of MCP Apps inline widgets.dispose() to release resources, and optionally update(context)
to receive context changes without remounting. The host aborts the mount’s signal
on disposal. A failing panel shows a local fallback while stock views remain
available. Keep the registration array stable across React renders.
For the stock served bundle, list developer-hosted modules at the top level of
console.toml. Each module’s default export is an array of panel definitions:
baseUrl. Realm overlays
can replace the list, including panel_modules = []. Reload after changing it.
This is a frontend loading option; tools do not supply these modules. The
application host owns module integrity and should serve versioned, audited assets.
Each panel needs a unique namespaced id, a title, and a mount function.
Console adds its sidebar button and pane-menu entry and owns tabs, splits and
layout restoration. Removing a registration leaves a restored unavailable
placeholder. It does not execute missing code or call a tool.
ConsolePanelContext supplies baseUrl, experience, readOnly, the host’s
authority, current selection, and the panel’s pinned conversation. Use
openPanel(id, { intent: "split_right" }) for another registered developer panel.
Optional instanceKey, JSON params, and a pure validateParams function allow
multiple instances. Layout storage contains selectors only: never put private
results or secrets in panel parameters. Set followSelection: true to follow the
selected conversation instead of pinning it. Context and pending requests are
discarded when the host authority changes. Layout keys include the authority
and principal scope; opening and restoring conversation
panels checks current visibility.
Panels can use context.request({ path, method, body }, signal). The default
service uses same-origin authenticated requests, rejects redirects, blocks
non-GET operations in view-only mode, and never retries a request. It sends
X-Console-Authority and X-Console-Conversation as URI-encoded JSON locators.
These headers are not authorization proof: the application must derive the viewer
from its authenticated session and enforce access on every request. Panels are
trusted application code, so browser checks do not replace server policy.
Embedded hosts may inject panelService(request, scope, signal) to use their
existing application service, forwarding the scope locators through its own
authenticated API. The host still applies view-only and lifetime
checks. The request scope includes the original panel conversation, not whichever
conversation happens to be focused later.
See examples/console-extensions for a module and local stock Console preview.
Inline MCP Apps
Inline widgets use the MCP Apps standard. An MCP server supplies tool metadata, aui:// HTML resource, and the original
tool result. No panel module or Console-specific app manifest is needed.
Enable the stock host in config/console.toml:
npm install:
--write-mcp-apps-sandbox <dir> writes two files, creating the directory and
replacing earlier copies, prints each file’s SHA-256 and path, and exits
without starting a gateway:
The proxy loads
src/mcp-apps/sandbox.js relative to its own file. The two
files and the Console embedded in the gateway share an internal sandbox
protocol, so write them again from the new binary whenever you upgrade the
gateway. The Python package and the npm package do not include the proxy.
Gateways before 0.9.2 do not embed the proxy. For those, copy the two
repository files from the release tag that matches your gateway into one
directory, keeping the layout above, and check your copies against these
SHA-256 digests (identical for 0.9.0 and 0.9.1):
The originating member profile selects its MCP servers through
tools.mcp_servers, or uses its existing host tool provider. Global server
configuration alone does not grant every member access to those tools.
The bundled gateways enable native MCP Apps negotiation when this sandbox URL
is configured. Rust library hosts opt in with UnifiedRuntime::builder().mcp_apps(true)
and set ConsolePolicy.ui.mcp_apps_sandbox_url on their Console policy.
Hosts that create their own external MCP connections must also negotiate the
standard UI extension, for example with McpAppsClientServiceFactory; this
cannot be added after a connection’s handshake.
Headless hosts omit the capability. An unavailable widget retains its ordinary
text tool result.
The host retains the original tool, arguments, complete result and available UI
resource with the canonical session. The declared HTML is preloaded before tool
execution; the app receives invocation inputs and results through MCP Apps
messages. Reopening a retained view never repeats
its original tool call or waits for the current agent turn. The resolved
canCallTools capability describes access to the live host action surface;
actual calls still require fresh native admission and the original connection.
During a continuing model turn, an actor- and run-bound native observation
exposes accepted tool results for display without changing committed history.
Completed-run replay uses the runtime’s committed session. These observations
carry no action authority. UI-only _meta is excluded from model requests and the
public transcript. Timeline frames carry locators, not private HTML or metadata.
Every lookup rechecks the authenticated viewer and session ownership. Uncached
resource reads and app actions also require agent.send, an editable Console
and the original live MCP connection with negotiated MCP Apps; replacing or
retiring it cannot silently redirect an old widget. View-only users can read
the exact retained renderer without fresh member-credential IO. Disabling the
sandbox URL also disables the Console’s app HTTP operations.
Tool visibility follows _meta.ui.visibility: omitted permits model and app,
["model"] permits only the model, ["app"] permits only the app, and []
permits neither. A model-only launch tool can still own a view. App actions use
fresh native authorization, pre- and post-tool hooks and normal execution
policy, including review when required. A post-hook refusal withholds the result
while preserving already settled effects. View-only users cannot invoke them.
App resource reads and tool execution can proceed while the model turn is still
running. A response with no session effects or hook notices does not wait for
that turn or trigger a session save, so several mounted widgets can poll for
fresh data. If the call produces session effects or hook notices, the native
actor applies and persists them before publishing the response; this settlement
can wait for the active turn. This distinction follows the actual execution
outcome, not a tool’s readOnlyHint. Each request still requires fresh admission
and the original actor and MCP connection.
The current host profile supports inline rendering, resource reads and tool
calls. It does not advertise conversation handoff, model-context updates,
external links or downloads. The sandbox restricts fetches and frames through
CSP and denies sensitive browser permissions. It rejects CSP grants covering
configured Console hostnames. Browser support limits network containment:
Chromium 140 ignores the emitted standard webrtc 'block' directive. Navigation
retirement is best-effort lifecycle handling, with every fresh action still
subject to native admission. Serve the sandbox without Console cookies or
application APIs; production hosts should use a dedicated HTTPS origin.
An ordinary MCP App can render a versioned generated document from its result
data today. The app supplies the renderer; the runtime does not impose a
component language. Keep generated content inside the app sandbox, separate
from trusted developer panels, and route external actions through the same
native admission path. Streaming updates and conversation handoff require their
own explicit lifecycle and provenance contracts when added.
Runtime contract
The bundled console uses a small set of stable protocol surfaces:/console/modules returns IDs from RuntimeDecisionState.modules. It is a configuration inventory, not confirmation of successful startup, current liveness, or health.
The console contract version is 0.5.0 in console/src/lib/contract.ts and docs/rct/console-rest-sse-contract-v0.5.0.json. The runtime JSON-RPC contract is separately versioned by META_CONTRACT_VERSION.
Gateway custody also differs. rpc_gateway installs a SQLite console timeline
store only when its init request configures persistent_state; long-lived
--persistent process mode alone does not make the timeline durable. Without
persistent state, its timeline stays in memory. The console-only
meta_gateway also keeps its timeline in memory. Do not infer timeline
durability from the presence of the same browser shell and route names.
Layout
The console uses a configurable workbench layout built on the sharedConsoleWorkbench component:
Sidebar - controls and roster
The left column is navigation. It can show stock controls such as topology, roster, logs, health, and approvals, plus app-defined buttons. Clicking an agent opens chat; details and profile-style data belong in roster surfaces. Agents can be grouped by configured selectors. The default useful pattern is labels first, then structural fallbacks:control, link to an href, or dock an operator panel. A panel is a same-origin path (one leading /, at most 2048 UTF-8 bytes, no whitespace, control characters or backslashes, and no . or .. segment in any spelling, %2e included) that the console shows in a docked frame. The path is used exactly as written; surrounding whitespace is refused, not trimmed. It cannot be combined with control or href; an invalid button is dropped.
The console and flow editor documents are served with Content-Security-Policy: frame-ancestors 'self', so only their own origin can frame them. A deployment that frames the console from another origin must change before Meta 0.9; same-origin framing (for example behind a reverse proxy on the console’s origin) and top-level use are unaffected. A panel page that a reverse proxy serves outside the gateway must send its own framing policy; the gateway’s header does not cover responses it does not serve.
Operator panels depend on a few host constraints:
- The console sets no
frame-srcrestriction, so same-origin panel paths stay framable. A host’s own same-origin review page can be configured as a panel. - The panel frame is not sandboxed, so a host page’s form posts keep their real
Origin. A sandbox withoutallow-same-originwould sendOrigin: nulland break same-origin form admission. - A page served with
frame-ancestors 'none', such as a secret-entry page, cannot be framed and must not be configured as a panel.
- The agent’s display name or, for an agent without one, its identity in words
(
router:mainreads “Router main”; a generated id reads as the agent’s kind and the id’s last six characters, such as “Reviewer 4e5f60”) - A second line with what the agent is (its role, else its group) and what it
is doing in words, or how long ago it was last active, for example
Router · workingorRouter · active 3 min ago - A pin action on hover, and beside it, when you point at the pin or reach the row with the keyboard, a copy action: pinned agents move to a Pinned section at the top, and the copy action copies the agent’s identity, which the row’s tooltip also shows
Ordering and grouping the roster
Two controls under the search box set how the roster lists agents:- Group: Category lists agents under their categories and subgroups (the
[agent_list]grouping above); None lists every agent in one list. Either way, Pinned stays on top and an agent spawned by another, or a side chat, stays nested under it. - Sort: Manual keeps your own order; Name sorts by display name, with
numbers in order (
Agent 2beforeAgent 10); Last activity lists the most recently active agents first and agents with no known activity last.
/console/experience reports as last_activity_ms, or a newer event the
console has since seen. History the console rebuilds, lifecycle records
(respawn, retire, reset, delete), interaction failures and memory events do not
count. Reading it wakes no agent.
The browser keeps these preferences in local storage, per signed-in user and
console: the manual order, the Group and Sort choices and the pins follow the
user and are kept when the [agent_list] configuration changes. The order of
categories and subgroups and which of them are collapsed belong to that
configuration and start over when it changes. Pins an earlier console kept
carry over once.
Non-addressable agents (e.g., Gate, Health Monitor) appear dimmed and cannot be messaged.
An identity-first agent whose identity binding moved (after a reset or a
delete), and that the console could not collect again within 2 s, has state
unknown and offers no send or retire until a later read collects it. Its
records carry an unobserved marker with reason refresh_pending (see
GET /console/experience). The
topbar shows the mob as unknown, with an amber dot, when the health snapshot
does not say whether the mob runs; a stopped mob’s dot is red.
Approvals
Open Approvals from the sidebar or Needs you to inspect current requests. Needs you also counts host-provided attention items when the console configuration names[[attention.sources]]. The inbox lists them after the approvals, as cards with one
Open link to the host’s page, which owns the decision. A source that cannot be read is
named and hides nothing else (see host-provided attention items).
A source with a decide_href gives a read-only console a Decide in link on each pending approval, in the inbox and in an agent’s chat, to the host page that decides it.
Expand Complete request details before deciding. Stale or unavailable approval
data does not mean there are no pending requests; use Refresh approvals to
reload it. Read-only consoles cannot submit decisions. Where the connection
offers no approvals (a gateway without meta/gating/pending, or an aggregate
console, whose inbox lists host attention items only), the Approvals header
reads Not available on this connection instead of pending and automatic
counts.
Risk tiers display in words with their code: R0 Minimal, R1 Low, R2 Elevated and
R3 High. A request’s chip reads “High risk”, with the tier code in its tooltip, and
an audit row reads “Risk: High (R3)”. A missing tier reads “Unknown”; an
unrecognized one reads “Unknown” followed by the recorded value. The current Meta
R2 path normally allows with audit.
It does not run the proposed native model reviewer. An approval record does not
confirm execution: the caller remains responsible for enforcing the returned
outcome and carrying out any permitted action. See Gating for
current tier, timeout, and decision behavior.
The stock console submits decisions with
approver_id: "console-ops-lead". There
is no approver selector in this inbox. A request pinned to a different approver
needs a host-provided approval surface that authenticates and maps the approver.
Configure action tiers through the host’s gating configuration. For SDK-launched
stdio gateways, see gating_config_path.
That option does not configure unrelated HTTP or library entrypoints.
Dock — Chat panels
The center area is a multi-panel tabbed workspace. Each panel contains aConversationPane with a ConsoleComposer.
Tab strip at the top:
- Click a tab to switch panels
+to create a new empty tab×to close a tab
- Textarea with
Message {agent}...placeholder - Send button (↑) — also supports Enter to send, Shift+Enter for newline
- Footer shows: target agent, member ID, profile, and status
Side chats
Fork to side chat in a conversation’s header branches the agent’s conversation into a side chat: a new member that starts from the agent’s committed transcript and opens in a pane beside the agent. You continue the side chat on its own; the agent’s session, transcript and queued work stay as they were.- The side chat runs with the agent’s role, profile and model overrides and tools, and inherits the agent’s labels.
- Its audience is exactly the agent’s: every agent check on a side chat is the agent’s, so whoever may see the agent, send to it or retire it may do the same with its side chats, a deny on the agent hides them, and a grant on the side chat’s own name grants nothing (see Side chats).
- It is never wired to a peer, whatever the agent’s role: no other agent can
address it, and none lists it as a peer. The runtime seats it exempt from
the mob definition’s wiring rules (
auto_wire_orchestrator,role_wiring), so no agent seated after it is wired to it either, and the definition-wiring reconcile and the topology’s declared edges leave it out, as they leave out every member exempt from those rules. A host’s own edge discovery policy is never handed one either. - It is listed under its agent in the sidebar, marked Side chat, and is
kept until you close it: across a console reload and a gateway restart. It
never idle-retires. Close side chat in its pane retires it, which, as
every check on a side chat, needs
agent.retireon its agent; the agent is not touched.meta/reset_allretires side chats with the other members that have no registered identity. A host that reconciles the whole mob against a declared roster (UnifiedRuntime::reconcile, without identity-first authority) retires them too, since no declared roster names them; the stock gateway does not. - A side chat is never respawned or reset: its pane offers neither, and
meta/respawn,meta/respawn_memberandmeta/resetrefuse it (side_chat_restart_refused), since its successor would be no fork of the agent. An agent’smob_respawntool refuses it too, andmeta/reset_allreports it as a member it could not reset where a host declares a baseline member under its name. Close it and fork the agent again. - A side chat is never adopted as an identity:
mob/adopt_member_identity_declarationrefuses it (side_chat_adoption_refused), since an adopted identity is rebuilt from its declaration, which no fork made. - The action is offered only to a viewer who may spawn members
(
agent.spawn) and send to the agent (agent.send), and only where the gateway’s sessions persist or its composition did not record whether they do. It is disabled while the agent works and enabled when it is idle. An agent seated and never woken has no conversation to fork: the fork is refused, and the agent is not woken for it. An agent the console’s visibility policy hides cannot be forked, nor can an agent whose lineage already fills the access rules’ walk (the agent and seven ancestors), by any viewer, an admin included. - Under
runtime_options.member_auth_binding, an agent whose session records another binding or none (one created before the launch named the binding) cannot be forked: the runtime would seat the side chat off the binding. A side chat forked before a launch named the binding is refused at its rebuild under that launch; close it and fork again. - Instructions the agent activated (
meta/member_activate_instruction) are inherited when they are in effect in the agent at the fork. Any other inherited instruction stays inactive, and the side chat’s pane names it: the side chat cannot answer until a host activates it.
meta/fork_member on the console RPC.
Schedules
An agent’s details pane (Details in the roster, or Inspect agent in its conversation’s header) lists the agent’s schedules: messages the Console sends to the agent later, as the person who defined them. A viewer who may message the agent adds one with New schedule: the message, an optional name, and when to send it, either once at a date and time, repeating every so many minutes, hours or days (the first send one interval from now unless you set a start), or daily at a time of day, on the weekdays you pick, in a named time zone. Each schedule shows when it sends, its next send while it is active and, to a viewer who may message the agent, who it sends as, with Edit, Pause or Resume, and Delete, which asks you to confirm. An edit keeps the timing unless you change it.- Each send is a Console send by the schedule’s sender, judged when it goes
out: the Console must still admit the sender as it would admit them then
(sign-in where it is required, the allowlist, the trusted issuer, and no
sessions revoked since the schedule was defined), and the send needs what
a message sent then would need,
agent.sendon the agent under the access policy at that time, a writable console, and an agent that takes console messages. A refused send fails that time only; the schedule keeps its later times. - Creating, editing or resuming a schedule makes you its sender. On a console without sign-in the sender is the console’s shared account, as it is for a message you send there; once the console requires sign-in, such schedules stop sending until someone signed in edits them.
- A change that races another change of the same schedule is refused and the list is read again: check the schedule and repeat the change.
- A schedule repeats at most once a minute.
- Viewers who may see the agent but not message it see its schedules read-only, and so does everyone on a read-only console or on a gateway that does not offer schedule changes; the pane says why.
- An agent whose input is governed takes no schedule (a governed send needs its human present, and a scheduled send has none), and an agent that takes no console messages cannot be scheduled.
- The pane lists the schedules defined in the Console only. The agent’s own schedules (its schedule tools) are not listed, and the agent’s tools neither see nor change the Console’s.
meta/schedule/* methods on the console RPC (see
Console schedules).
Local action feedback
When a host supplies a recognized native refusal or audit fact, the Console shows feedback for the affected action. A permitted sibling or later reply can still appear in the same run.
Custom workspace hosts should retain
ConversationMessageEntry.operationFeedback
when transforming shared timeline entries. The reusable ConversationTranscript
renders this metadata automatically. A custom renderer can display its title,
detail and supplied toolCallId or operationId; preserve those identifiers
verbatim. The shared projection classifies supported typed payloads, rather than
interpreting arbitrary tool messages as decisions.
Cards cover recognized current typed payloads. An unknown cause or malformed
payload does not acquire a permission or confinement classification. Unrecognized
tool results use the ordinary tool-result display and may show raw output; no
dedicated fallback card is promised for every event. A missing card is not a permission decision or evidence
of success. Preserve unfamiliar payloads in the host’s authorized diagnostics.
Keep run state attached to the controller’s runtime lifecycle events. A local
feedback card does not end a run. Rendering these cards does not configure
native authorization, process confinement or durable audit; the host supplies
those runtime capabilities separately.
Voice conversations
When the gateway has an authenticated OpenAI Live configuration, a blue waveform button appears beside Send. Voice is not offered without OpenAI authentication, in a view-only console, or for an agent you cannot message. Microphone access requires HTTPS or localhost and your browser’s permission. Source builds must enable theopenai-live Cargo feature. Public GPT Live
requires a compatible OpenAI API-key binding; a ChatGPT OAuth login alone
does not enable this feature.
Voice is an explicit gateway registration, not a setting in console.toml.
For meta_gateway, supply console_voice in the init parameters alongside
your auth_config and enable persistent_sessions:
summary block bounds the concurrent context summary (see
“Context preparation” below). Every field is optional: model defaults to
the background agent’s own text model, max_input_bytes to 65536 (64 KiB)
and max_output_bytes to 4096 (4 KiB); zero is rejected. The input bound
degrades rather than fails: when the newest message alone exceeds it (a
large tool result, say), that one whole message is summarised; only
Meerkat’s 4 MiB capture ceiling refuses a snapshot outright. In Rust the
same knobs are PublicLiveRegistration.summary (ConsoleVoiceSummaryConfig);
the SDK builders take them on OpenAiLiveGatewayConfig:
rpc_gateway, place the same console_voice registration under
runtime_options and use the gateway’s persistent session configuration.
The SDK builders expose this as .consoleVoice(config) in TypeScript and
.console_voice(config) in Python, using OpenAiLiveGatewayConfig.
A Rust host that embeds UnifiedRuntime through its builder registers the
same thing with .console_voice(PublicLiveRegistration) (plus .live(...)
for the external channel) and serves
build_reference_app_router_with_live(decisions); the decision state must
require app auth, for example parse_console_auth_config with
provider: "oidc" for a Google-fronted console. See the Unified Runtime guide.
Credentials stay on the server. The console checks readiness for the selected
agent before offering voice and checks it again before microphone capture;
registration or model availability alone does not enable the button.
Click the waveform button to start a gpt-live-1 conversation with the
selected agent as its background agent. Audio connects without waiting for a
summary of that agent’s context; it does not change the background agent’s text model.
The voice strip above the composer names the voice agent and shows separate
waveforms for your microphone and the live model’s audio.
The microphone is requested during the click, alongside the gateway readiness
check (Allow microphone access); a negative readiness releases it again
without opening a channel. Startup then distinguishes Starting voice from
Connecting audio. As soon as the microphone is open the primary status reads
Listening, you can talk; nothing about context gates it. Context
preparation is a secondary hint beside that status: Context arriving while
the initial summary is read, generated, and sent (the tooltip names the stage),
then Context supplied once the provider acknowledged it, which does not
mean the model has recalled or spoken it. Context failures stay visible
without disabling the connected audio; a failed status read is shown separately
and retried.
Context preparation is a separate, tool-free summary request using the background
agent’s credentials; it does not change that agent’s model or reasoning
settings. It reads the most recent 64 KiB of the serialized transcript (older
turns are dropped whole; a size bound, not a content heuristic; a newest
message larger than the window is kept whole), asks for
compact notes rather than replayed logs, uses low or disabled OpenAI reasoning
where the model catalog supports it, and caps the result at 4 KiB. The public
GPT Live transport delivers the summary in UTF-8 fragments of at most 500
bytes, each waiting for its own provider receipt, so the 4 KiB cap bounds
delivery at nine fragments; the prompt itself aims for about 200 words. The
summary model defaults to the background agent’s own model because Meerkat’s
catalog exposes a support tier, not a speed tier; set summary.model to a
faster model on the same provider when the agent’s model is slow to first
token. A produced summary is cached per member, keyed by the source session’s
transcript cursor and a digest of the exact summarised window, so reopening
voice on an unchanged transcript reuses it without another model call. The
defaults come from measurement: with the previous 4 MiB input and 16 KiB
output bounds a 40-turn session took 4 to 6 seconds to summarise; the 64 KiB
window keeps that cost flat as histories grow. Meerkat orders the captured
history and newer conversation updates, and cancels or fences context work
when the call closes or changes. A context job from an old call cannot update
a replacement call.
The microphone and speaker controls are independent. Muting speakers silences
local playback, not the conversation; the model’s output waveform continues
to show incoming audio. End the conversation with the close button.
Voice stays with its original agent when you select another chat, switch
tabs, or open an operator panel. Starting voice on another agent ends the
previous voice conversation before opening the new one. Text composers remain
usable throughout, including for the agent currently in voice mode.
A gateway may also register live: true beside console_voice so an external
client (for example an embodiment daemon opening meta/live/open over
{base}/live/ws) shares the same live host. The two take turns: the newest
engagement wins. When the external client opens a channel, the console call
ends with “Voice moved to the external live channel”, and readiness for the
console reports available: false with reason: "external_live_active" and
the holder until that channel closes. Starting voice again from the console
takes the path back and closes the external channel with a typed
superseded_by_console_voice reason.
With GPT Live client delegation, typed messages go to the background agent;
the composer labels that destination during voice. Its canonical context and
results are mirrored into the voice conversation. This follows
OpenAI’s typed-input contract:
typed text is user data for the backend, not a live-model instruction or a
native spoken turn. Chats with other agents do not change the voice target.
For local session-backed agents, console messages use explicit conversational
user admission, preserving the console interaction identity in canonical
history. Internal events and recalled memory are not relabeled as human input.
Voice ends automatically after 15 minutes of silence, not after 15
minutes of total call time. Text messages do not extend that audio inactivity
window. Ending voice or closing the console releases the browser’s microphone
and playback resources. The close button immediately mutes microphone
transmission and playback, retaining the muted WebRTC transport for up to five
seconds while the server finalizes. Transport and capture resources are then
released even if finalization fails; an unconfirmed close remains retryable.
If Meerkat provides a recovery connection, the console
reconnects to the same voice agent without changing your mute settings;
unrecoverable audio connection loss ends voice.
Voice acceptance tests
The offline browser lane uses real local WebRTC peers without paid provider calls. The opt-in live lane starts a separate authenticated console and uses realgpt-live-1, real gpt-5.5 background/peer calls, and original synthetic
speech fixtures instead of capturing a person’s microphone:
scripts/repo-cargo.
Set META_VOICE_GATEWAY_BIN to an already-built current gateway to reuse it.
Missing credentials fail the selected live lane; they do not produce a skip
that looks like a passing acceptance run. The runner does not use an existing
console or record microphone input.
Coverage includes spoken requests, typed-value recall, peer results arriving
after an initial answer, and close/reopen while background work remains pending.
Expected native transcript content must have fresh non-silent audio nearby;
packets, old audio peaks, or mirrored voice text masquerading as a background
answer are not sufficient. Late ordinary background facts may reach the new
voice conversation through current canonical context, but old receipts must
not control it.
The public API does not expose pending canonical-mirror acknowledgements.
Exact close while an append awaits its provider acknowledgement is therefore
covered separately by deterministic Meerkat owner tests. The paid console lane
does not infer that hidden state from an unfinished background task.
The optional summary-latency benchmark makes three billed gpt-5.5 calls and
checks a five-second summary budget while retaining exact context values:
Signals rail - event feed
The right column shows user-meaningful signals: requests, assistant replies, peer messages, image events, relevant tool activity, and alert levels. Low-level transport envelopes, scaffold prompts, raw checksum tokens, and raw tool metadata are filtered out before reaching the user-facing view.WorkGraph panel - Tree and Graph views
The WorkGraph panel (sidebar nav entry, visible when the runtime wires a WorkGraph service) readsmeta/workgraph/snapshot and offers two views of the same data via the Tree | Graph toggle in the panel head. Tree is the default: an indented item list with claim/close actions plus the attention-binding and recent-event sections. Graph draws a read-only layered DAG - roots leftmost, one column per parent-edge depth, solid arrows into parents, dashed amber blocks edges, node borders/dots in the item-status palette (pulsing blue in progress, green completed, red failed, amber blocked, struck-through cancelled). Drag to pan, wheel to zoom, Fit resets, clicking a node shows its id/status/owner detail underneath. The view caps at 200 nodes and reports the overflow; all mutations stay in the tree and attention sections.
Agent grouping
Agents are grouped in the sidebar byconsole_config.agent_list. Selectors support labels.<key>, label:<key>, raw label keys, and direct fields such as group, subgroup, role, kind, identity, member_id, and agent_id.
Prefer mob member labels for domain-specific grouping:
Well-known labels
Console configuration
The stock console can be shaped byconfig/console.toml in the conventional workspace layout. meta_gateway loads that file automatically; rpc_gateway accepts runtime_options.console_config_path, and the TypeScript builder exposes .consoleConfig("config/console.toml").
The parsed config is projected through GET /console/experience as console_config.
CSS customization
The console uses the shared@console-components stylesheet with --cc-* CSS custom properties. Override these on a wrapper element with data-cc-theme="dark" (or "light").
Key tokens
Theming
Without configuration or saved preferences, the stock console defaults to light mode and the Rams variant.console_config.appearance can supply different defaults; saved user preferences take precedence. Shared-component hosts select light or dark mode with data-cc-theme="light" or "dark" on their .cc-theme-scope root.
The visible console canvas and sidebar surfaces use theme/variant tokens rather than fixed colors. For example, #131316 is the graphite-dark canvas color, not a universal body background. The shared stylesheet’s fallback sidebar safe-top and left/right padding are 10px and 12px/12px; the stock host overrides them with the values in the table.
Resizable panels
Both the sidebar and activity rail are resizable by dragging the edge dividers:- Sidebar divider — drag the right edge of the sidebar (min 180px, max 420px)
- Activity rail divider — drag the left edge of the activity rail (min 200px, max 480px)
Experience metadata
The console fetches its initial state fromGET /console/experience, which returns agent data, topology, and health information. The key field for the sidebar is agent_sidebar.live_snapshot.agents:
"addressable" and "internal_only".
Messaging protocol
1
Seed the timeline
Panels load recent visible frames with
GET /console/timeline?identity=identity:alice&mode=recent&limit=.... Older history is paged with before; live continuation uses latest_cursor.2
Send message
Identity-addressed panels call Returns
meta/console/send through POST /console/rpc:{ "interaction_id": "...", "identity": "..." }.origin is the caller’s free-form identifier for audit and routing.
origin_kind is optional and typed: operator, operator_probe,
connector, scheduler, policy, flow or system. It is stored on
the user_input frame, and the transcript labels the turn from it
(“Operator”, “Operator probe”, “Scheduled turn”, …). Any other value
is refused. Automated checks such as release-gate probes should send
operator_probe; the stock console sends operator. A declared
origin_kind is part of the idempotent request, so a retry must repeat it.idempotency_key must be unique per send; the stock console mints one
UUID per send. An idempotent retry reuses the same key in the same
caller/runtime/identity/origin namespace with identical content and
effective handling_mode (default queue). The caller is the
authenticated principal (anonymous callers of an open console share one
local account), so another user’s identical key is a different send. An
open console without an access controller does not resolve tokens, so all
of its callers share that one account; isolating users needs
require_app_auth or an access controller.
The original acceptance is returned and the turn is not run again. Changed
content or handling mode in that namespace is rejected with HTTP 409 /
JSON-RPC -32009 idempotency_conflict. A different identity or origin is
a different send namespace, not a guaranteed replay.A retry of a send the gateway accepted but stopped before submitting is
dispatched then, once, into the session the send recorded; a retry of a
send the runtime already admitted only settles it. meta/console/recover_send
(POST /console/send/recover) reports a send’s status and what the runtime
holds for it, for the caller’s own sends only.skill_refs is optional: typed, source-pinned skills selected for this
turn ([{"kind": "structured", "source_uuid": "...", "skill_name": "..."}],
the same shape as meerkat turn/start). The addressed member resolves
them natively and commits a durable SkillContext; the console never
renders skill bodies into the message. A nonempty selection is part of the
idempotent request: a replay must repeat the same selection in the same
order, and a changed selection under the same key is
-32009 idempotency_conflict. Only local session-backed members accept a
selection.The legacy POST /console/send route still exists for compatibility. New console clients should prefer JSON-RPC.3
Receive response frames
The timeline stream delivers canonical console frames:This is the SSE wire envelope:
type wraps a frame with kind and
payload. The frontend normalizes these into its internal event/data
representation; that normalized object is not the SSE wire shape.Correlate a send with its terminal by interaction_id on frames whose
source.kind is console_event. Session-history backfill re-emits past
turns as frames with source.kind = "session_history"; they carry the
persisted interaction id of the turn they replay and are never fresh
completions, so clients MUST exclude them from live turn handling (the
rule is stated with the frame shape under meta/console/query_timeline
in the JSON-RPC reference).If a requested cursor is no longer replayable, the stream returns 409 replay_unavailable; refetch a recent page and resume from its latest_cursor.A replay from a cursor is sent as it is read, one page at a time. If the
log stops holding what follows while the stream replays (it was
cleared), the stream sends a replay_unavailable event and ends: handle
it as the 409. If a later page cannot be read, the stream ends after the
frames it sent: reconnect with the last frame’s cursor as
Last-Event-ID.Console routes
Primary RPC methods on
/console/rpc:
meta/console/send- identity-addressed console sendmeta/console/query_timeline- timeline replay querymeta/console/list_identitiesandmeta/console/inspect_identity- identity-first inspectionmeta/blob/upload- multipart blob uploadmeta/reload_member- non-destructive cold reload of one identity’s live member (same session, same generation); the repair for a member whose sends fail with the reload-required class. Gated by theagent.respawnaction.meta/member_healthis the matching read (agent.view): lifecycle state, last delivery error class, and the actor-loop probe verdict with the openstall_id, from in-process reads that answer during a stall.meta/identity/warm- builds one identity’s member without a turn and writes its tool record, with the result and error codes of the stdio gateway’smeta/identity/warm. Gated by theagent.respawnaction.meta/retire,meta/respawn,meta/reset, andmeta/reset_all- operator lifecycle actions.meta/retirereleases the member, which stays seated on its session at its generation. A member that no identity holds and no host adopted, such as a side chat, is retired by Core instead: it leaves the roster with the members it spawned.reset_allresets every identity the identity runtime knows, whatever its state (generation advance, oneidentity_resetlifecycle frame each, listed underreset): each reset is Core’s rotation, which seats adormantidentity Core holds no member for on a fresh session too, and starts abrokenone over. It retires only members with no registered identity, such as raw delegates (listed underretired_delegates); a member that is retiring appears in no list. A registered identity on a runtime without a session bridge fails the preflight withidentity_reset_requires_session_bridgeand nothing is touched. An identity whose own reset fails is listed underfailed, the identities beside it are still reset, and the call answers-32000with the full body inerror.datameta/send_message- legacy member-addressed send pathmeta/fork_member- forks an agent’s conversation into a side chat (see Side chats); gated byagent.spawnfor the side chat andagent.viewandagent.sendfor the agent, and offered where sessions persist or the composition did not record whether they dometa/schedule/list,meta/schedule/create,meta/schedule/update,meta/schedule/pause,meta/schedule/resumeandmeta/schedule/delete- an agent’s console schedules (see Schedules); the list is gated byagent.viewand the writes byagent.sendfor the agent, and each send is a Console send by the schedule’s sendermeta/topology/query,meta/topology/plan,meta/topology/apply,meta/topology/operation/get, andmeta/topology/audit/query- optional, revisioned connection management and separately permissioned durable audit
rt:<identity>:<generation>. After a reset, the predecessor’s
runtime id is refused with -32000 and error.data.kind
stale_identity_runtime_binding, naming the current binding
(registered_runtime_member_id, registered_session_id). A hidden identity’s
runtime id answers identity_hidden_by_policy, and a runtime id of an
identity the runtime does not know answers identity not found.
meta/console/inspect_identity refuses a stale runtime id the same way
before it looks the identity up in the console aggregator. A runtime id
whose identity’s binding cannot be read right now is never let through: it
is refused as unreadable, retryable, with error.data.kind
identity_history_unavailable. The code is -32052 when the route’s
identity lookup or its runtime id check meets the failed read, and can be
-32000 when the route’s own status read fails after its lookup, or when
the id names a live member whose binding cannot be read.
Topology management is opt-in at the runtime. With the default disabled
policy, the stock console remains a passive topology viewer and does not show
the Connections tab. Read-only mode exposes searchable connection state with
server-provided denial reasons. Editable mode offers only pairwise actions
authorized for both endpoints; bulk controls appear only when a host supplies
an explicitly bounded action and the runtime advertises topology.bulk.
The reusable TopologyPanel keeps the authority-local and bilateral CAS
contracts distinct. Stock JSON-RPC hosts supply the scalar query revision.
Same-process coordinator hosts also supply the exact
authority_revisions map as authorityRevisions; mutation callbacks receive
that map as expectedAuthorityRevisions and must pass it to the bilateral
coordinator as expected_revisions. Cross-authority hosts must never collapse
that map to the scalar local revision or route the intent through the local
JSON-RPC adapter. They may use topologyAuthorityRevisionToken as the
component’s scalar view token when the coordinator snapshot has no scalar
revision, but never as the coordinator CAS value.
Editable mode requires a durable runtime state path; Meta fails closed
rather than accepting reconnect/suppression intent that would disappear on
restart:
Authentication
Coarse console access is governed byConsolePolicy:
require_app_auth defaults to true. The SDK builders expose the opt-out as
consoleAuthRequired(false) (TypeScript) and console_auth_required(False)
(Python); rpc_gateway reads it as runtime_options.console_require_app_auth.
When require_app_auth is true, protected console JSON, RPC, blob, and stream
requests are checked against the AuthPolicy. The shell, static assets, and
/healthz remain public. See authentication for the
exact HTTP surface boundary.
When read_only is true, the console can inspect identities, timelines,
logs, and topology, but chat sends, uploads, lifecycle controls, and other write
RPCs are denied server-side.
For per-principal and per-agent authorization, install Meta’s optional
AccessController from config/access.toml or the runtime builder. When
enabled, its ABAC policy is deny-by-default with deny-overrides across console
REST, JSON-RPC, and SSE. JSON-RPC denials use code -32030 with
data.kind = "access_denied"; REST and SSE return HTTP 403. mob.observe
authorizes mob-level observation but does not grant agent.view for every
agent. To observe everything, a principal needs both mob.observe and an
applicable agent.view grant.
In Console access, open Groups and choose Inspect access beside a
member to prefill that exact subject in Preview. Select an action and
optional agent, then choose Evaluate to ask the access controller. The
shortcut does not evaluate automatically or change policy, and remains
available to authorized administrators on a read-only connection. A preview is
an observation, not permission for a later action or a native tool grant.
Upstream hosts that already know a user’s ACL can also force the stock console UI
into view-only mode without changing Meta auth: set
window.__META_CONSOLE_READ_ONLY__ = true before loading the console bundle,
or append ?console_read_only=true (also accepted:
meta_console_read_only=true or view_only=true) to the console URL. This is
a frontend-only affordance for per-user UX; direct RPC calls are still governed
by the server-side ConsolePolicy.read_only setting.
Console mutation endpoints assume non-ambient credentials: pass bearer tokens
explicitly in the Authorization header or auth_token query parameter for
SSE/browser bootstrap cases. If a host proxy converts console auth into cookies
or another browser-ambient credential, that host must add origin and CSRF
protection before exposing mutation routes such as /console/rpc,
/console/rpc/multipart, or /console/send.
Build system
The console has two build modes:
The production build resolves
@console-core and @console-components as local workspace packages via esbuild aliases. The browser bundle and CSS are embedded in the gateway binary.
Development workflow
63220, but currently omits
/console/timeline (including /stream), /console/identities, and /blobs.
Full-contract development through Vite requires adding proxy entries for
those paths to the same backend. Alternatively, use the embedded console at
http://127.0.0.1:63220/console for full-contract validation; rebuild the
production console assets and Rust host to test frontend changes there.
When using Vite, edits in console/src/ or packages/ appear instantly in the browser.
Architecture
console-core and console-components) avoid app-store and Electron/Desktop-bridge coupling. console-components owns presentation; console-core includes a Meta HTTP/SSE transport using web APIs and a controller that accepts injected host transports. These are private workspace packages, not independently published npm API promises.
See also
- Unified runtime — how the console is composed with the runtime
- SSE API — protocol details
- Authentication — console access control
